From 69fdf6a58b72b534fc3a9776248b12dcf4c1afd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 13:30:57 +0000 Subject: [PATCH] fix(http): reject CR/LF/NUL in set_headers values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The set_headers action lets operators add response headers whose values are templated strings, e.g. "response_headers": { "X-Echo": "$uri" } A template can dereference request-controlled state (URI, query arguments, headers). If the request URI contains "%0D%0A", the template substitution produces a value with embedded CRLF, and the wire serialiser writes "X-Echo: prefix\r\nInjected: x\r\n" — standard HTTP response splitting. Reject any computed value that contains CR, LF, or NUL. An unsafe value is dropped (same semantics as a NULL template result) and an INFO line names the header so an operator can diagnose a misconfigured template without exposing the offending request payload at higher log levels. Static config values are operator-controlled and conventionally trusted, but the check is two compares per byte and applies uniformly to both code paths. Failing the request would help an attacker probe for the protection; silently dropping the header keeps the response well-formed. --- src/nxt_http_set_headers.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/nxt_http_set_headers.c b/src/nxt_http_set_headers.c index 7fd6aba52..1ca6df2ff 100644 --- a/src/nxt_http_set_headers.c +++ b/src/nxt_http_set_headers.c @@ -68,6 +68,29 @@ nxt_http_set_headers_init(nxt_router_conf_t *rtcf, nxt_http_action_t *action, } +/* + * Reject values that would inject a header boundary into the response. + * Templated values (e.g. $uri, $arg_*) can carry CR/LF/NUL bytes if the + * client encodes them in the request, and writing those bytes verbatim + * into the wire serialiser yields HTTP response splitting. Static + * config values are operator-controlled and trusted, but the check is + * cheap enough to apply to both paths. + */ +static nxt_bool_t +nxt_http_header_value_is_safe(const nxt_str_t *v) +{ + size_t i; + + for (i = 0; i < v->length; i++) { + if (v->start[i] == '\r' || v->start[i] == '\n' || v->start[i] == '\0') { + return 0; + } + } + + return 1; +} + + static nxt_http_field_t * nxt_http_resp_header_find(nxt_http_request_t *r, u_char *name, size_t length) { @@ -144,6 +167,17 @@ nxt_http_set_headers(nxt_http_request_t *r) return NXT_ERROR; } } + + if (value[i].start != NULL + && nxt_slow_path(!nxt_http_header_value_is_safe(&value[i]))) + { + nxt_log(&r->task, NXT_LOG_INFO, + "set_headers \"%V\": dropping value containing CR, LF " + "or NUL (HTTP response-splitting protection)", + &hv->name); + value[i].start = NULL; + value[i].length = 0; + } } for (i = 0; i < n; i++) {