Merge branch 'main' of https://github.com/classroomio/classroomio into feat/release-v2

This commit is contained in:
Rotimi Best
2025-12-24 04:18:39 +00:00
193 changed files with 10584 additions and 6399 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { config as emailAndPassword } from './auth/email-password';
import { syncUserWithProfile } from './auth/hooks/sync-user';
export const auth = betterAuth({
export const auth: ReturnType<typeof betterAuth> = betterAuth({
baseURL: CONSTANTS.BASE_URL,
database: drizzleAdapter(db, {
provider: 'pg',
+2
View File
@@ -17,6 +17,7 @@
"@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/typography": "^0.5.18",
"@types/dompurify": "^3.2.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@@ -56,6 +57,7 @@
"@tiptap/suggestion": "^3.7.1",
"d3-array": "^3.2.4",
"d3-scale": "^4.0.2",
"dompurify": "^3.3.0",
"katex": "^0.16.22",
"lowlight": "^3.3.0",
"svelte-tiptap": "^3.0.0",
File diff suppressed because one or more lines are too long
+193
View File
@@ -0,0 +1,193 @@
/**
* HTML Sanitization utilities to prevent XSS attacks
*/
import DOMPurify from 'dompurify';
const browser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
/**
* Sanitizes HTML content using DOMPurify to prevent XSS attacks
* This provides robust client-side protection against XSS attacks
*/
export function sanitizeHtml(html: string): string {
if (typeof html !== 'string') return '';
// Only run DOMPurify in the browser
if (!browser) {
return fallbackSanitize(html);
}
let sanitized = DOMPurify.sanitize(html, {
// Allow safe HTML tags
ALLOWED_TAGS: [
'p',
'br',
'strong',
'b',
'em',
'i',
'u',
'span',
'div',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'ul',
'ol',
'li',
'blockquote',
'pre',
'code',
'a',
'img'
],
// Allow safe attributes
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'style'],
// Forbid dangerous attributes
FORBID_ATTR: [
'onerror',
'onload',
'onclick',
'onmouseover',
'onfocus',
'onblur',
'onchange',
'onsubmit',
'onreset',
'onselect',
'onunload'
],
// Forbid dangerous tags (including svg)
FORBID_TAGS: [
'script',
'iframe',
'object',
'embed',
'form',
'input',
'textarea',
'select',
'button',
'meta',
'link',
'svg',
'math'
],
// Keep content of forbidden tags
KEEP_CONTENT: true,
// Sanitize attributes
SANITIZE_DOM: true,
// Allow data attributes
ALLOW_DATA_ATTR: false,
// Transform URLs and filter dangerous protocols
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
});
// Additional post-processing to remove any SVG data URLs that might have slipped through
sanitized = sanitized.replace(/src\s*=\s*["']data:image\/svg[^"']*["']/gi, 'src=""');
return sanitized;
}
/**
* Fallback sanitization for server-side rendering
*/
function fallbackSanitize(html: string): string {
if (typeof html !== 'string') return '';
// Remove SVG tags and their content completely
html = html.replace(/<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>/gi, '');
html = html.replace(/<svg\b[^>]*\/>/gi, '');
// Remove script tags and their content
html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// Remove javascript: protocol from attributes
html = html.replace(/javascript:/gi, '');
// Remove data:image/svg URLs which can contain XSS
html = html.replace(/src\s*=\s*["']data:image\/svg[^"']*["']/gi, 'src=""');
// Remove on* event handlers (onclick, onload, onerror, etc.)
html = html.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '');
html = html.replace(/\s*on\w+\s*=\s*[^\s>]*/gi, '');
// Remove data: protocol for script execution
html = html.replace(/\bdata:\s*text\/html/gi, 'data:text/plain');
// Remove vbscript: protocol
html = html.replace(/vbscript:/gi, '');
// Remove expression() from CSS (IE specific XSS vector)
html = html.replace(/expression\s*\(/gi, '');
// Remove dangerous tags
const dangerousTags = [
'script',
'iframe',
'object',
'embed',
'form',
'input',
'textarea',
'select',
'button',
'svg',
'math'
];
dangerousTags.forEach((tag) => {
const regex = new RegExp(`<${tag}\\b[^>]*>.*?<\\/${tag}>`, 'gi');
html = html.replace(regex, '');
const selfClosing = new RegExp(`<${tag}\\b[^>]*\\/>`, 'gi');
html = html.replace(selfClosing, '');
const unclosed = new RegExp(`<${tag}\\b[^>]*>`, 'gi');
html = html.replace(unclosed, '');
});
return html;
}
/**
* Strips all HTML tags and returns plain text
* Use this when you only need the text content
*/
export function stripHtml(html: string): string {
if (typeof html !== 'string') return '';
return html.replace(/<[^>]*>/g, '');
}
/**
* Escapes HTML special characters to prevent XSS
* Use this when displaying user input as text
*/
export function escapeHtml(text: string): string {
if (typeof text !== 'string') return '';
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
};
return text.replace(/[&<>"'/]/g, (s) => map[s]);
}
/**
* Validates and sanitizes URLs to prevent javascript: and data: XSS vectors
*/
export function sanitizeUrl(url: string): string {
if (typeof url !== 'string') return '';
// Remove dangerous protocols
if (/^(javascript|vbscript|data|file):/i.test(url.trim())) {
return '';
}
return url;
}
+8
View File
@@ -32,6 +32,14 @@
"./validation/*": {
"types": "./src/validation/*/index.ts",
"default": "./src/validation/*/index.ts"
},
"./functions": {
"types": "./src/functions/index.ts",
"default": "./src/functions/index.ts"
},
"./functions/*": {
"types": "./src/functions/*/index.ts",
"default": "./src/functions/*/index.ts"
}
},
"scripts": {
@@ -0,0 +1,87 @@
/**
* Server-side file type validation utilities for preventing malicious uploads
* Specifically designed to prevent SVG XSS attacks and other security issues
*/
export const ALLOWED_IMAGE_TYPES = [
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp'
] as const;
export const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'] as const;
/**
* Validate file type by MIME type
* @param file - File object to validate
* @returns boolean indicating if file type is allowed
*/
export function validateImageType(file: File): boolean {
return ALLOWED_IMAGE_TYPES.includes(file.type as (typeof ALLOWED_IMAGE_TYPES)[number]);
}
/**
* Validate file extension
* @param filename - Filename to validate
* @returns boolean indicating if extension is allowed
*/
export function validateImageExtension(filename: string): boolean {
const extension = filename.toLowerCase().substring(filename.lastIndexOf('.'));
return ALLOWED_EXTENSIONS.includes(extension as (typeof ALLOWED_EXTENSIONS)[number]);
}
/**
* Comprehensive file validation
* @param file - File object to validate
* @returns object with validation result and error message
*/
export function validateImageUpload(file: File): { isValid: boolean; error?: string } {
// Check file type
if (!validateImageType(file)) {
return {
isValid: false,
error: 'Invalid file type. Only JPEG, PNG, GIF, and WebP files are allowed.'
};
}
// Check file extension (double check for security)
if (!validateImageExtension(file.name)) {
return {
isValid: false,
error: 'Invalid file extension. Only .jpg, .jpeg, .png, .gif, and .webp files are allowed.'
};
}
// Additional security checks
if (file.name.toLowerCase().includes('.svg')) {
return {
isValid: false,
error: 'SVG files are not allowed for security reasons.'
};
}
return { isValid: true };
}
/**
* Sanitize filename to prevent directory traversal and other issues
* @param filename - Original filename
* @returns sanitized filename
*/
export function sanitizeFilename(filename: string): string {
// Remove path traversal attempts
let sanitized = filename.replace(/[/\\]/g, '');
// Remove special characters that could cause issues
sanitized = sanitized.replace(/[<>:"|?*]/g, '');
// Limit length
if (sanitized.length > 255) {
const ext = sanitized.substring(sanitized.lastIndexOf('.'));
const name = sanitized.substring(0, sanitized.lastIndexOf('.'));
sanitized = name.substring(0, 255 - ext.length) + ext;
}
return sanitized;
}
+1
View File
@@ -0,0 +1 @@
export * from './fileValidation';