Skip to content

[Security] SSE Injection via unsanitized event fields in parseMessage() (CWE-93) #42

@SnailSploit

Description

@SnailSploit

Security Vulnerability Report

Severity: Medium (CVSS 5.4)
CWE: CWE-93 — Improper Neutralization of CRLF Sequences ('CRLF Injection')
Affected Version: sse-channel v4.0.0
Reporter: Kai Aizen (kai.aizen.dev@gmail.com)


Summary

The parseMessage() function in sse-channel concatenates user-controlled SSE fields (event, id, retry) directly into the SSE wire format without sanitizing newline characters. An attacker who controls these fields can inject additional SSE fields or entirely new SSE events into the stream, enabling event spoofing and client-side injection.

Vulnerable Code

In the message formatting logic, event, id, and retry values are concatenated raw:

// When constructing the SSE message string:
// event, id, and retry fields are interpolated directly
// without newline sanitization
if (msg.event) {
  output += 'event:' + msg.event + '\n';
}
if (msg.id) {
  output += 'id:' + msg.id + '\n';
}
if (msg.retry) {
  output += 'retry:' + msg.retry + '\n';
}

The event, id, and retry fields are concatenated directly into the output without any check for \n or \r characters.

Proof of Concept

const SseChannel = require('sse-channel');
const http = require('http');

const channel = new SseChannel();

// Attacker injects a newline + fake event via the 'event' field
channel.send({
  event: 'legit\ndata:injected-event\n',
  data: 'real-data'
});

Expected output (safe):

event:legit
data:real-data

Actual output (vulnerable):

event:legit
data:injected-event

data:real-data

The injected data:injected-event line followed by a blank line creates a completely new, attacker-controlled SSE event that the client parses as legitimate.

Impact

  • Event Spoofing: Attacker can inject arbitrary SSE events into the stream
    • Client-side Manipulation: Injected events can trigger unintended behavior in frontend JavaScript EventSource listeners
      • Data Integrity: Consumers of the SSE stream cannot distinguish injected events from legitimate ones

Suggested Fix

Sanitize newline characters (\n, \r) from the event, id, and retry fields before formatting:

function sanitize(value) {
  if (value === null || value === undefined) return value;
  return String(value).replace(/[\r\n]/g, '');
}

// Then use sanitize() when building the output:
if (msg.event) {
  output += 'event:' + sanitize(msg.event) + '\n';
}
if (msg.id) {
  output += 'id:' + sanitize(msg.id) + '\n';
}
if (msg.retry) {
  output += 'retry:' + sanitize(msg.retry) + '\n';
}

Related Advisory

This is part of a class of SSE injection vulnerabilities across multiple SSE libraries. See: GHSA-4hxc-9384-m385


Note: I attempted to report this via GitHub PVRT, but private vulnerability reporting is not enabled on this repository. This issue is filed for visibility. Please consider enabling PVRT for future security reports.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions