Skip to content

fix: add header validation to early_hints callback#3588

Merged
benoitc merged 2 commits into
benoitc:masterfrom
eddieran:fix/early-hints-header-validation
Apr 19, 2026
Merged

fix: add header validation to early_hints callback#3588
benoitc merged 2 commits into
benoitc:masterfrom
eddieran:fix/early-hints-header-validation

Conversation

@eddieran

Copy link
Copy Markdown
Contributor

Summary

Fixes #3585 — the wsgi.early_hints callback constructs 103 Early Hints responses with zero header validation, while Response.process_headers already validates header names against TOKEN_RE and header values against HEADER_VALUE_RE. This gap means a WSGI app passing unsanitized user input to wsgi.early_hints could enable HTTP response splitting via CRLF injection.

What this does

Adds the same TOKEN_RE.fullmatch(name) / HEADER_VALUE_RE.fullmatch(value) checks (and the value.strip(" \t") normalization) from process_headers into send_early_hints. Raises the same InvalidHeaderName / InvalidHeader exceptions on failure.

Addressing maintainer concerns

Frameworks should do it. Maybe we should ensure to not do the work twice.

Agreed — frameworks should validate. But gunicorn already validates in process_headers for normal responses, so this PR simply brings early_hints to parity. It's defense-in-depth: if the framework misses it, the server catches it. The regex checks are fast and only run on the (typically small) early hints header list, so the cost is negligible.

Test plan

  • Added tests for CRLF injection in header values → InvalidHeader raised, nothing written to socket
  • Added tests for CRLF injection in header names → InvalidHeaderName raised
  • Added tests for invalid token characters in header names → InvalidHeaderName raised
  • Verified valid headers still pass through correctly
  • All 11 existing + new early hints tests pass

The early_hints callback constructs 103 Early Hints responses without
any header validation, while process_headers validates against TOKEN_RE
and HEADER_VALUE_RE for normal responses. This inconsistency means a
WSGI app passing unsanitized data to wsgi.early_hints could enable
HTTP response splitting via CRLF injection.

Apply the same TOKEN_RE/HEADER_VALUE_RE checks from process_headers to
the early_hints callback for defense-in-depth consistency.

Closes benoitc#3585
Comment thread gunicorn/http/wsgi.py Outdated
if not TOKEN_RE.fullmatch(name):
raise InvalidHeaderName('%r' % name)
if not HEADER_VALUE_RE.fullmatch(value):
raise InvalidHeader('%r' % value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until error handling receives some major cleanup (preferably class inheritance instead of massive elif) that exception class should be instantiated with just the name, not the value. Otherwise any way of triggering the exception on purpose can be used move sensitive information between security boundaries. Browsers are picky about what headers can be accessed cross-site, and proxies sitting between Gunicorn and the client might pick which ones to forward. Best not to interfere with that.

Per @pajod review: the invalid header value may carry sensitive
content, and raising it through the exception could leak it
across security boundaries (browsers/proxies handling response
splitting errors). Pass just the name instead.
@eddieran

Copy link
Copy Markdown
Contributor Author

Addressed, @pajod — thanks for catching the cross-boundary leak. Changed raise InvalidHeader('%r' % value)raise InvalidHeader('%r' % name) so the value never appears in the exception. Agreed that the existing process_headers on line 350 has the same pattern, but since you flagged that it's pending the broader error-handling cleanup, I left it alone in this PR.

@benoitc benoitc merged commit e5c30b4 into benoitc:master Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wsgi.early_hints callback has no header validation (response splitting risk)

3 participants