Skip to content

Commit ba3c3fe

Browse files
committed
fix(sse): sanitize carriage returns in event stream data and comments
1 parent c56683d commit ba3c3fe

File tree

2 files changed

+24
-1
lines changed

2 files changed

+24
-1
lines changed

src/utils/sse/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function formatEventStreamMessage(message: EventStreamMessage): string {
1616
result += `retry: ${message.retry}\n`;
1717
}
1818
const data = typeof message.data === "string" ? message.data : "";
19-
for (const line of data.split("\n")) {
19+
for (const line of data.split(/\r\n|\r|\n/)) {
2020
result += `data: ${line}\n`;
2121
}
2222
result += "\n";

test/sse.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,26 @@ it("prevents data field injection of new events", () => {
159159
`data: hi\ndata: \ndata: event: system\ndata: data: INJECTED\n\n`,
160160
);
161161
});
162+
163+
it("sanitizes carriage returns in data to prevent SSE injection", () => {
164+
const result = formatEventStreamMessage({
165+
data: "legit\revent: evil",
166+
});
167+
// \r should be treated as a line break, not passed through
168+
expect(result).toBe(`data: legit\ndata: event: evil\n\n`);
169+
});
170+
171+
it("sanitizes \\r\\n in data field", () => {
172+
const result = formatEventStreamMessage({
173+
data: "line1\r\nline2\rline3\nline4",
174+
});
175+
expect(result).toBe(`data: line1\ndata: line2\ndata: line3\ndata: line4\n\n`);
176+
});
177+
178+
it("prevents event splitting via \\r\\r in data", () => {
179+
const result = formatEventStreamMessage({
180+
data: "first\r\rdata: injected",
181+
});
182+
// Double \r should produce an empty line, not a message boundary
183+
expect(result).toBe(`data: first\ndata: \ndata: data: injected\n\n`);
184+
});

0 commit comments

Comments
 (0)