Skip to content

HTTP: sanitize invalid add_header and add_trailer fields - #1414

Open
louzt wants to merge 1 commit into
nginx:masterfrom
louzt:upstream/response-filter-sanitize
Open

louzt wants to merge 1 commit into
nginx:masterfrom
louzt:upstream/response-filter-sanitize

Conversation

@louzt

@louzt louzt commented May 31, 2026 •

Copy link
Copy Markdown

Summary

This splits the late response-filter part of the CRLF hardening discussion from #590 into a standalone change for add_header and add_trailer field values.

It covers value sanitization only. Field-name validation is intentionally not in scope here, per the maintainer direction from 2026-06-21 that names are static and cannot be attacker-controlled, so value sanitization is the runtime concern worth shipping first.

Why

The proxy request construction path and the response filter path have different failure timing.

For proxy request construction, rejection can happen before unsafe bytes are emitted upstream.

For add_header and especially add_trailer, the invalid field can be discovered late enough that a clean rejection path is not a good fit for the response lifecycle.

This patch keeps the response alive while preventing invalid value bytes from producing malformed serialized fields.

What changed

  • adds a small helper in ngx_http_headers_filter_module.c that scans a computed value and returns it unchanged when no NUL/CR/LF is present;
  • allocates a same-length request-pool copy only for values that contain those bytes;
  • replaces NUL/CR/LF with SP in that copy;
  • if a sanitized copy cannot be allocated, returns NGX_ERROR so the parent filter aborts in the same style as an ngx_http_complex_value() failure;
  • applies this in both add_header and add_trailer before serialization.

Patch shape

1 file changed, 44 insertions(+)

Validation

  • debug build on the patched branch: header_filter_sanitize.t passes 6/6;
  • ASAN/UBSAN build on the patched branch: header_filter_sanitize.t passes 6/6;
  • current nginx/nginx master with the same test: fails 3/6 because add_header/add_trailer can serialize an injected X-Injected field line.

Risks / Follow-ups

This is a policy choice for the late response-filter path, not a claim that sanitization is the only acceptable behavior.

If upstream prefers rejection instead, add_header likely needs earlier validation while add_trailer probably needs a dedicated late-body/trailer failure strategy rather than the same return path used by earlier serialization boundaries.

Field-name validation is intentionally out of scope here and remains a separate concern: static config-time validation can be discussed independently if maintainers prefer that boundary.

@github-actions

github-actions Bot commented May 31, 2026 •

Copy link
Copy Markdown

✅ All required contributors have signed the F5 CLA for this PR. Thank you!
Posted by the CLA Assistant Lite bot.

@louzt

louzt commented May 31, 2026

Copy link
Copy Markdown
Author

Companion nginx-tests draft: nginx/nginx-tests#58

@DemiMarie

Copy link
Copy Markdown

You’ll need to sign the F5 CLA before this can be looked at.

@DemiMarie

Copy link
Copy Markdown

Also, what about field names? Those also need sanitization. Would it be possible to simply skip the whole field if the name is bad? That matches the behavior of typical applications, which ignore unknown headers.

@louzt

louzt commented May 31, 2026

Copy link
Copy Markdown
Author

I have hereby read the F5 CLA and agree to its terms

@louzt louzt changed the title HTTP: sanitize invalid add_header and add_trailer values HTTP: sanitize invalid add_header and add_trailer fields May 31, 2026
@louzt

louzt commented May 31, 2026

Copy link
Copy Markdown
Author

Fair point.

add_header / add_trailer names are static in this directive, but this module was not validating them before serializing the field. I updated the draft to cover that too.

Current shape on the branch:

  • invalid value bytes (NUL / CR / LF) are normalized to SP in a request-pool copy;
  • invalid field names are skipped entirely;
  • valid sibling fields are still emitted.

I also updated the companion test so it now covers both cases:

  • current nginx/nginx master: header_filter_sanitize.t fails 4/14;
  • this PR branch: header_filter_sanitize.t passes 14/14;
  • ASAN/UBSAN build of this branch: header_filter_sanitize.t passes 14/14.

I agree that skipping the whole field is the better shape for bad names here.

@github-project-automation github-project-automation Bot moved this from New to In Progress in NGINX OSS Unified Workspace May 31, 2026

@DemiMarie DemiMarie left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good so far!

@louzt
louzt marked this pull request as ready for review May 31, 2026 05:49
@DemiMarie

Copy link
Copy Markdown

The invalid-name handling here is intentionally a skip policy in the response-filter layer. Config-time rejection of bad static names could still be discussed separately if maintainers prefer that boundary.

#590 (comment) suggests that config-time validation for these directives would be a good idea:

HTTP field name/value pairs:

  • grpc_set_header
  • proxy_set_header
  • add_header
  • add_trailer
  • proxy_hide_header
  • fastcgi_hide_header

CGI-style name/value pairs:

  • fastcgi_param
  • uwsgi_param
  • scgi_param

The first six are different than the last three because (to the best of my knowledge) *CGI accept only a subset of what HTTP allows as headers. I belive the regex is ^[A-Za-z_][A-Za-z0-9_]*$ (same as C ASCII identifiers) but am not at all certain.

This should be a separate PR, though.

@DemiMarie

Copy link
Copy Markdown

Validating the names can be dropped from this PR. Validating the values is much more important, as the names are static and so cannot be attacker-controlled.

@louzt
louzt force-pushed the upstream/response-filter-sanitize branch from f91fde6 to 1792031 Compare August 7, 2026 22:35
@louzt

louzt commented Aug 7, 2026

Copy link
Copy Markdown
Author

Rebased onto current master and dropped the 3 stale name-validation commits so the C code matches the value-only scope Demi's 2026-06-21 direction requested. Body refreshed to reflect the new patch shape (44 insertions) and the 6/6 test pass on the patched branch. Lab notes and branch matrix: https://gist.github.com/louzt/7bdf370a28126718e7e7b69d53b0ae86

@DemiMarie DemiMarie left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good!

@itsgaldoron

Copy link
Copy Markdown

Takeover notice (same root cause, disclosure): #1933 fixes this root cause at the serialization layer and supersedes this PR as the complete fix. This PR has been quiet since 2026-08-07 (last activity is yours) and ships no test against this repository.

Disclosing the relationship openly rather than racing it:

If you return and would rather land this PR first, say so here and #1933 can be rebased on top of it — the serialization sites still need closing either way. If the maintainers prefer your narrower layer instead, #1933 can be closed; no objection from me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants