Skip to content

Commit 840ac5c

Browse files
committed
fix(sse): sanitize newlines in event stream fields to prevent SSE injection
1 parent 24231b9 commit 840ac5c

File tree

2 files changed

+48
-3
lines changed

2 files changed

+48
-3
lines changed

src/utils/sse/utils.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,26 @@ import { EventStreamMessage } from "./types";
77
export function formatEventStreamMessage(message: EventStreamMessage): string {
88
let result = "";
99
if (message.id) {
10-
result += `id: ${message.id}\n`;
10+
result += `id: ${_sanitizeSingleLine(message.id)}\n`;
1111
}
1212
if (message.event) {
13-
result += `event: ${message.event}\n`;
13+
result += `event: ${_sanitizeSingleLine(message.event)}\n`;
1414
}
1515
if (typeof message.retry === "number" && Number.isInteger(message.retry)) {
1616
result += `retry: ${message.retry}\n`;
1717
}
18-
result += `data: ${message.data}\n\n`;
18+
const data = typeof message.data === "string" ? message.data : "";
19+
for (const line of data.split("\n")) {
20+
result += `data: ${line}\n`;
21+
}
22+
result += "\n";
1923
return result;
2024
}
2125

26+
function _sanitizeSingleLine(value: string): string {
27+
return value.replace(/[\n\r]/g, "");
28+
}
29+
2230
export function formatEventStreamMessages(
2331
messages: EventStreamMessage[],
2432
): string {

test/sse.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,40 @@ it("properly formats multiple sse messages", () => {
122122
]);
123123
expect(result).toEqual(`data: hello world\n\nid: 1\ndata: hello world 2\n\n`);
124124
});
125+
126+
it("sanitizes newlines in event field to prevent SSE injection", () => {
127+
const result = formatEventStreamMessage({
128+
event: "message\nevent: admin\ndata: INJECTED",
129+
data: "legit",
130+
});
131+
expect(result).toEqual(
132+
`event: messageevent: admindata: INJECTED\ndata: legit\n\n`,
133+
);
134+
expect(result.split("\n").filter((l) => l.startsWith("event:")).length).toBe(
135+
1,
136+
);
137+
});
138+
139+
it("sanitizes newlines in id field to prevent SSE injection", () => {
140+
const result = formatEventStreamMessage({
141+
id: "1\ndata: INJECTED",
142+
data: "legit",
143+
});
144+
expect(result).toEqual(`id: 1data: INJECTED\ndata: legit\n\n`);
145+
});
146+
147+
it("splits multi-line data into separate data fields", () => {
148+
const result = formatEventStreamMessage({
149+
data: "line1\nline2\nline3",
150+
});
151+
expect(result).toEqual(`data: line1\ndata: line2\ndata: line3\n\n`);
152+
});
153+
154+
it("prevents data field injection of new events", () => {
155+
const result = formatEventStreamMessage({
156+
data: "hi\n\nevent: system\ndata: INJECTED",
157+
});
158+
expect(result).toBe(
159+
`data: hi\ndata: \ndata: event: system\ndata: data: INJECTED\n\n`,
160+
);
161+
});

0 commit comments

Comments
 (0)