|
1 | 1 | import type { Response } from 'express'; |
2 | 2 |
|
3 | 3 | /** |
4 | | - * Sanitizes a string to prevent XSS attacks when sent via SSE |
5 | | - * Removes potentially dangerous HTML/script content |
| 4 | + * Escapes special characters in JSON strings using Unicode escapes. |
| 5 | + * This prevents potential XSS if JSON is ever interpreted as HTML, |
| 6 | + * while keeping the JSON valid. |
| 7 | + * |
| 8 | + * JSON.stringify handles standard JSON escaping, but doesn't escape |
| 9 | + * <, >, & which could be problematic if the response is misinterpreted. |
6 | 10 | */ |
7 | | -function sanitizeString(str: string): string { |
8 | | - return str |
9 | | - .replace(/</g, '<') |
10 | | - .replace(/>/g, '>') |
11 | | - .replace(/&/g, '&') |
12 | | - .replace(/"/g, '"') |
13 | | - .replace(/'/g, '''); |
| 11 | +function escapeJsonString(jsonStr: string): string { |
| 12 | + return jsonStr |
| 13 | + .replace(/</g, '\\u003c') |
| 14 | + .replace(/>/g, '\\u003e') |
| 15 | + .replace(/&/g, '\\u0026'); |
14 | 16 | } |
15 | 17 |
|
16 | 18 | /** |
17 | | - * Recursively sanitizes all string values in an object |
18 | | - */ |
19 | | -function sanitizeObject(obj: unknown): unknown { |
20 | | - if (typeof obj === 'string') { |
21 | | - return sanitizeString(obj); |
22 | | - } |
23 | | - |
24 | | - if (Array.isArray(obj)) { |
25 | | - return obj.map(sanitizeObject); |
26 | | - } |
27 | | - |
28 | | - if (obj !== null && typeof obj === 'object') { |
29 | | - const sanitized: Record<string, unknown> = {}; |
30 | | - for (const [key, value] of Object.entries(obj)) { |
31 | | - sanitized[key] = sanitizeObject(value); |
32 | | - } |
33 | | - return sanitized; |
34 | | - } |
35 | | - |
36 | | - return obj; |
37 | | -} |
38 | | - |
39 | | -/** |
40 | | - * Creates a safe SSE sender function that sanitizes data before sending |
41 | | - * This prevents XSS attacks from user-provided or error message content |
| 19 | + * Creates a safe SSE sender function. |
| 20 | + * |
| 21 | + * Security measures: |
| 22 | + * 1. JSON.stringify handles escaping for JSON context |
| 23 | + * 2. Unicode escapes for <, >, & prevent HTML interpretation |
| 24 | + * 3. Content-Type: text/event-stream prevents browser HTML rendering |
| 25 | + * 4. X-Content-Type-Options: nosniff prevents MIME sniffing |
42 | 26 | */ |
43 | 27 | export function createSafeSSESender(res: Response) { |
44 | 28 | return (data: object) => { |
45 | | - // Sanitize all string values in the data to prevent XSS |
46 | | - const sanitizedData = sanitizeObject(data); |
47 | | - res.write(`data: ${JSON.stringify(sanitizedData)}\n\n`); |
| 29 | + // JSON.stringify provides safe JSON encoding |
| 30 | + // Additional unicode escapes for <, >, & as defense-in-depth |
| 31 | + const jsonData = escapeJsonString(JSON.stringify(data)); |
| 32 | + res.write(`data: ${jsonData}\n\n`); |
48 | 33 | }; |
49 | 34 | } |
50 | 35 |
|
51 | 36 | /** |
52 | | - * Sanitizes an error message for safe inclusion in SSE responses |
| 37 | + * Sanitizes an error message for safe inclusion in responses. |
| 38 | + * Uses Unicode escapes instead of HTML entities to keep the message |
| 39 | + * valid for JSON contexts while preventing XSS. |
53 | 40 | */ |
54 | 41 | export function sanitizeErrorMessage(error: unknown): string { |
55 | 42 | const message = |
56 | 43 | error instanceof Error ? error.message : 'An unexpected error occurred'; |
57 | | - return sanitizeString(message); |
| 44 | + // Use unicode escapes for safety (same as escapeJsonString but for plain strings) |
| 45 | + return message |
| 46 | + .replace(/</g, '\\u003c') |
| 47 | + .replace(/>/g, '\\u003e') |
| 48 | + .replace(/&/g, '\\u0026'); |
58 | 49 | } |
59 | 50 |
|
60 | 51 | /** |
|
0 commit comments