Skip to content

Commit 8f64fe6

Browse files
committed
refactor(api): update SSE utilities to enhance security and sanitization
1 parent 24dd3f0 commit 8f64fe6

File tree

1 file changed

+30
-39
lines changed

1 file changed

+30
-39
lines changed

apps/api/src/utils/sse-utils.ts

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,51 @@
11
import type { Response } from 'express';
22

33
/**
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.
610
*/
7-
function sanitizeString(str: string): string {
8-
return str
9-
.replace(/</g, '&lt;')
10-
.replace(/>/g, '&gt;')
11-
.replace(/&/g, '&amp;')
12-
.replace(/"/g, '&quot;')
13-
.replace(/'/g, '&#x27;');
11+
function escapeJsonString(jsonStr: string): string {
12+
return jsonStr
13+
.replace(/</g, '\\u003c')
14+
.replace(/>/g, '\\u003e')
15+
.replace(/&/g, '\\u0026');
1416
}
1517

1618
/**
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
4226
*/
4327
export function createSafeSSESender(res: Response) {
4428
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`);
4833
};
4934
}
5035

5136
/**
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.
5340
*/
5441
export function sanitizeErrorMessage(error: unknown): string {
5542
const message =
5643
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');
5849
}
5950

6051
/**

0 commit comments

Comments
 (0)