BUG-HUNT: [security] SseEventFormatter.format() does not sanitize event_type or event_id for newlines — SSE header injection allows protocol-level response spoofing #6693

Open
opened 2026-04-09 23:38:59 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: Security — SSE Header Injection via Unsanitized event_type / event_id

Severity Assessment

  • Impact: An attacker who can publish a crafted A2aEvent to the A2aEventQueue can inject arbitrary SSE control lines into a streaming response, spoofing status events, fake error payloads, or injecting false data: frames for other subscribers/clients.
  • Likelihood: Medium — requires the ability to publish an event (e.g. via EventBusBridge forwarding an attacker-controlled domain event, or directly via a compromised internal service).
  • Priority: High

Location

  • File: src/cleveragents/a2a/events.py
  • Class: SseEventFormatter
  • Function: format()
  • Lines: 204–212

Description

SseEventFormatter.format() builds the SSE text block using Python f-strings, directly interpolating event.event_type and event.event_id into SSE protocol lines without sanitizing for newline characters (\n, \r\n):

lines = [
    f"event: {event.event_type}",  # ← no newline sanitization
    f"id: {event.event_id}",        # ← no newline sanitization
    f"data: {data_payload}",
    "",
    "",
]
return "\n".join(lines)

The Server-Sent Events specification (WHATWG EventSource) assigns special meaning to newlines: each newline terminates a field. A \n embedded in event_type terminates the event: line early and starts a new SSE field, allowing an attacker to inject arbitrary additional lines.

Evidence — Proof of Injection

The A2aEvent validator in models.py only rejects empty/whitespace event_type — it does not strip or reject newline characters:

# models.py lines 172-176
@field_validator("event_type")
@classmethod
def _event_type_non_empty(cls, value: str) -> str:
    if not value or not value.strip():
        raise ValueError("event_type must not be empty")
    return value   # ← newlines pass through unchanged

A crafted event:

A2aEvent(
    event_type="TaskStatusUpdateEvent\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"task/cancel\",\"params\":{}}",
    plan_id="victim-plan-id",
    data={},
)

Would produce the SSE stream output:

event: TaskStatusUpdateEvent
data: {"jsonrpc":"2.0","method":"task/cancel","params":{}}
id: 01HXRC...
data: {"jsonrpc":"2.0","method":"task/statusUpdate","params":{...}}


The injected data: line precedes the real one and appears as a separate SSE event to the client, spoofing a cancellation notification.

The same injection is possible through event_id (also interpolated without sanitization).

Expected Behavior

SSE field values must not contain newline characters. The formatter should strip or reject any \n, \r\n, or \r in event_type, event_id, and plan_id before interpolating them into SSE header lines. RFC 7231 / WHATWG EventSource spec requires field values to be single-line.

Actual Behavior

Newlines in event_type or event_id are passed directly into the SSE output, splitting the field at the newline and injecting new SSE fields.

Suggested Fix

Option A — Sanitize in SseEventFormatter.format() (defensive; preferred):

def _sanitize_sse_field(value: str) -> str:
    """Remove newline characters that would break SSE framing."""
    return value.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")

lines = [
    f"event: {_sanitize_sse_field(event.event_type)}",
    f"id: {_sanitize_sse_field(event.event_id)}",
    f"data: {data_payload}",
    "",
    "",
]

Option B — Validate in A2aEvent:
Add a validator that rejects event_type / event_id containing \n or \r:

@field_validator("event_type", "event_id")
@classmethod
def _no_newlines(cls, value: str) -> str:
    if "\n" in value or "\r" in value:
        raise ValueError("SSE field values must not contain newline characters")
    return value

Both options should be applied together for defence in depth.

Category

security / data-injection

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: Security — SSE Header Injection via Unsanitized `event_type` / `event_id` ### Severity Assessment - **Impact**: An attacker who can publish a crafted `A2aEvent` to the `A2aEventQueue` can inject arbitrary SSE control lines into a streaming response, spoofing status events, fake error payloads, or injecting false `data:` frames for other subscribers/clients. - **Likelihood**: Medium — requires the ability to publish an event (e.g. via `EventBusBridge` forwarding an attacker-controlled domain event, or directly via a compromised internal service). - **Priority**: High ### Location - **File**: `src/cleveragents/a2a/events.py` - **Class**: `SseEventFormatter` - **Function**: `format()` - **Lines**: 204–212 ### Description `SseEventFormatter.format()` builds the SSE text block using Python f-strings, directly interpolating `event.event_type` and `event.event_id` into SSE protocol lines **without sanitizing for newline characters** (`\n`, `\r\n`): ```python lines = [ f"event: {event.event_type}", # ← no newline sanitization f"id: {event.event_id}", # ← no newline sanitization f"data: {data_payload}", "", "", ] return "\n".join(lines) ``` The Server-Sent Events specification (WHATWG EventSource) assigns special meaning to newlines: each newline terminates a field. A `\n` embedded in `event_type` terminates the `event:` line early and starts a new SSE field, allowing an attacker to inject arbitrary additional lines. ### Evidence — Proof of Injection The `A2aEvent` validator in `models.py` only rejects empty/whitespace `event_type` — it **does not** strip or reject newline characters: ```python # models.py lines 172-176 @field_validator("event_type") @classmethod def _event_type_non_empty(cls, value: str) -> str: if not value or not value.strip(): raise ValueError("event_type must not be empty") return value # ← newlines pass through unchanged ``` A crafted event: ```python A2aEvent( event_type="TaskStatusUpdateEvent\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"task/cancel\",\"params\":{}}", plan_id="victim-plan-id", data={}, ) ``` Would produce the SSE stream output: ``` event: TaskStatusUpdateEvent data: {"jsonrpc":"2.0","method":"task/cancel","params":{}} id: 01HXRC... data: {"jsonrpc":"2.0","method":"task/statusUpdate","params":{...}} ``` The injected `data:` line **precedes** the real one and appears as a separate SSE event to the client, spoofing a cancellation notification. The same injection is possible through `event_id` (also interpolated without sanitization). ### Expected Behavior SSE field values must not contain newline characters. The formatter should strip or reject any `\n`, `\r\n`, or `\r` in `event_type`, `event_id`, and `plan_id` before interpolating them into SSE header lines. RFC 7231 / WHATWG EventSource spec requires field values to be single-line. ### Actual Behavior Newlines in `event_type` or `event_id` are passed directly into the SSE output, splitting the field at the newline and injecting new SSE fields. ### Suggested Fix **Option A — Sanitize in `SseEventFormatter.format()`** (defensive; preferred): ```python def _sanitize_sse_field(value: str) -> str: """Remove newline characters that would break SSE framing.""" return value.replace("\r\n", " ").replace("\r", " ").replace("\n", " ") lines = [ f"event: {_sanitize_sse_field(event.event_type)}", f"id: {_sanitize_sse_field(event.event_id)}", f"data: {data_payload}", "", "", ] ``` **Option B — Validate in `A2aEvent`**: Add a validator that rejects `event_type` / `event_id` containing `\n` or `\r`: ```python @field_validator("event_type", "event_id") @classmethod def _no_newlines(cls, value: str) -> str: if "\n" in value or "\r" in value: raise ValueError("SSE field values must not contain newline characters") return value ``` Both options should be applied together for defence in depth. ### Category `security` / `data-injection` ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
Author
Owner

Verified — Security bug: SSE header injection via unsanitized event_type/event_id. MoSCoW: Must-have. Priority: High.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Security bug: SSE header injection via unsanitized event_type/event_id. MoSCoW: Must-have. Priority: High. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6693
No description provided.