Guard against invalid id/event values in Server Sent Events

Prior to this commit, our implementation of Server Sent Events (SSE),
`SseEmitter` (MVC) and `ServerSentEvent` (WebFlux), would not guard
against invalid characters if the application mistakenly inserts such
characters in the `id` or `event` types.
Both implementations would also behave differently when it comes
to escaping comment multi-line events.

This commit ensures that both implementations handle multi-line comment
events and reject invalid characters in id/event types.
This commit also optimizes `String` concatenation and memory usage
when writing data.

Fixes gh-36440
This commit is contained in:
Brian Clozel
2026-03-10 17:56:58 +01:00
parent 37e8aa76e9
commit 6e9758700a
6 changed files with 183 additions and 25 deletions
@@ -20,6 +20,7 @@ import java.time.Duration;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -239,16 +240,23 @@ public final class ServerSentEvent<T> {
@Override
public Builder<T> id(String id) {
checkEvent(id);
this.id = id;
return this;
}
@Override
public Builder<T> event(String event) {
checkEvent(event);
this.event = event;
return this;
}
private static void checkEvent(String content) {
Assert.isTrue(content.indexOf('\n') == -1 && content.indexOf('\r') == -1,
"illegal character '\\n' or '\\r' in event content");
}
@Override
public Builder<T> retry(Duration retry) {
this.retry = retry;
@@ -40,7 +40,6 @@ import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@code HttpMessageWriter} for {@code "text/event-stream"} responses.
@@ -48,6 +47,7 @@ import org.springframework.util.StringUtils;
* @author Sebastien Deleuze
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
*/
public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Object> {
@@ -129,8 +129,9 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
result = Flux.just(encodeText(sseText + "\n", mediaType, factory));
}
else if (data instanceof String text) {
text = StringUtils.replace(text, "\n", "\ndata:");
result = Flux.just(encodeText(sseText + text + "\n\n", mediaType, factory));
StringBuilder sb = new StringBuilder(sseText);
writeStringData(text, sb);
result = Flux.just(encodeText(sb.toString(), mediaType, factory));
}
else {
result = encodeEvent(sseText, data, dataType, mediaType, factory, hints);
@@ -140,6 +141,31 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
});
}
private void writeStringData(String input, StringBuilder sb) {
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
sb.append(input);
}
else {
int length = input.length();
for (int i = 0; i < length; i++) {
char c = input.charAt(i);
if (c == '\r') {
if (i + 1 < length && input.charAt(i + 1) == '\n') {
i++;
}
sb.append("\ndata:");
}
else if (c == '\n') {
sb.append("\ndata:");
}
else {
sb.append(c);
}
}
}
sb.append("\n\n");
}
@SuppressWarnings("unchecked")
private <T> Flux<DataBuffer> encodeEvent(CharSequence sseText, T data, ResolvableType dataType,
MediaType mediaType, DataBufferFactory factory, Map<String, Object> hints) {