Summary
The SSE Emitter writes event and id parameters directly into the response stream without stripping newline characters. If application code passes user-controlled data as an event name or ID to SseClient.sendEvent(), an attacker can inject arbitrary SSE fields into the stream.
Details
In javalin/src/main/java/io/javalin/http/sse/Emitter.kt, the emit(event, data, id) method constructs SSE frames by string concatenation:
if (id != null) {
write("id: $id$NEW_LINE") // no newline sanitization
}
write("event: $event$NEW_LINE") // no newline sanitization
The SSE specification (https://html.spec.whatwg.org/multipage/server-sent-events.html) defines events as newline-delimited fields. A newline inside the event or id value terminates that field prematurely, and any text after the newline is parsed as a new field. This allows injection of data:, event:, id:, and retry: lines.
The sendComment() path already handles this correctly:
fun emit(comment: String) =
try {
comment.split(NEW_LINE).forEach {
write("$COMMENT_PREFIX $it$NEW_LINE")
}
And the data field is also handled safely by reading line-by-line. Only event and id lack this protection.
Steps to Reproduce
Application code:
app.sse("/live") { client ->
val eventType = ctx.queryParam("type") ?: "update"
client.sendEvent(eventType, jsonData)
}
Request: GET /live?type=x%0Adata:%20%7B%22injected%22:true%7D%0A%0Aevent:%20update
This produces:
event: x
data: {"injected":true}
event: update
data: <legitimate data>
The browser's EventSource delivers {"injected":true} as a complete event, injecting a fake event into the stream.
Impact
- Injection of arbitrary data payloads that client-side JavaScript processes as trusted server events
- Manipulation of the
Last-Event-ID reconnection header via injected id: field
- Modification of the client reconnection interval via injected
retry: field
Suggested Fix
Strip \r and \n from event and id before writing:
val sanitizedId = id?.replace(Regex("[\r\n]"), "")
val sanitizedEvent = event.replace(Regex("[\r\n]"), "")
Summary
The SSE
Emitterwriteseventandidparameters directly into the response stream without stripping newline characters. If application code passes user-controlled data as an event name or ID toSseClient.sendEvent(), an attacker can inject arbitrary SSE fields into the stream.Details
In
javalin/src/main/java/io/javalin/http/sse/Emitter.kt, theemit(event, data, id)method constructs SSE frames by string concatenation:The SSE specification (https://html.spec.whatwg.org/multipage/server-sent-events.html) defines events as newline-delimited fields. A newline inside the
eventoridvalue terminates that field prematurely, and any text after the newline is parsed as a new field. This allows injection ofdata:,event:,id:, andretry:lines.The
sendComment()path already handles this correctly:And the
datafield is also handled safely by reading line-by-line. Onlyeventandidlack this protection.Steps to Reproduce
Application code:
Request:
GET /live?type=x%0Adata:%20%7B%22injected%22:true%7D%0A%0Aevent:%20updateThis produces:
The browser's
EventSourcedelivers{"injected":true}as a complete event, injecting a fake event into the stream.Impact
Last-Event-IDreconnection header via injectedid:fieldretry:fieldSuggested Fix
Strip
\rand\nfromeventandidbefore writing: