Skip to content

Commit 79cabe3

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

File tree

2 files changed

+30
-2
lines changed

2 files changed

+30
-2
lines changed

src/utils/internal/event-stream.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export function isEventStream(input: unknown): input is EventStream {
180180
export function formatEventStreamComment(comment: string): string {
181181
return (
182182
comment
183-
.split("\n")
183+
.split(/\r\n|\r|\n/)
184184
.map((l) => `: ${l}\n`)
185185
.join("") + "\n"
186186
);
@@ -198,7 +198,7 @@ export function formatEventStreamMessage(message: EventStreamMessage): string {
198198
result += `retry: ${message.retry}\n`;
199199
}
200200
const data = typeof message.data === "string" ? message.data : "";
201-
for (const line of data.split("\n")) {
201+
for (const line of data.split(/\r\n|\r|\n/)) {
202202
result += `data: ${line}\n`;
203203
}
204204
result += "\n";

test/unit/sse.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,34 @@ describe("sse (unit)", () => {
7474
expect(result).toEqual(`: hello\n: data: INJECTED\n\n`);
7575
});
7676

77+
it("sanitizes carriage returns in data to prevent SSE injection", () => {
78+
const result = formatEventStreamMessage({
79+
data: "legit\revent: evil",
80+
});
81+
// \r must be treated as a line break, so "event: evil" becomes a data: line
82+
expect(result).toBe(`data: legit\ndata: event: evil\n\n`);
83+
});
84+
85+
it("sanitizes \\r\\n in data field", () => {
86+
const result = formatEventStreamMessage({
87+
data: "line1\r\nline2\rline3\nline4",
88+
});
89+
expect(result).toBe(`data: line1\ndata: line2\ndata: line3\ndata: line4\n\n`);
90+
});
91+
92+
it("prevents event splitting via \\r\\r in data", () => {
93+
const result = formatEventStreamMessage({
94+
data: "first\r\rdata: injected",
95+
});
96+
// \r\r should produce an empty line between, not a message boundary
97+
expect(result).toBe(`data: first\ndata: \ndata: data: injected\n\n`);
98+
});
99+
100+
it("sanitizes carriage returns in comments to prevent injection", () => {
101+
const result = formatEventStreamComment("x\rdata: injected");
102+
expect(result).toBe(`: x\n: data: injected\n\n`);
103+
});
104+
77105
describe("EventStream", () => {
78106
it("onClosed does not cause unhandled rejection when callback throws", async () => {
79107
const event = mockEvent("/");

0 commit comments

Comments
 (0)