[http_splitting] HTTP splitting (CRLF injection)
What this check looks for
This plugin looks for cases where user-controlled input can end up inside response headers (add_header, return, rewrite) or inside the request NGINX sends upstream (proxy_set_header, proxy_pass), through variables that can contain newline characters:
- regex captures that are not restricted to a safe character set,
$uriand$document_uri, which hold the decoded URI, so%0d%0abecomes a line break,$request_body, which is raw client input,$remote_user, which NGINX base64-decodes from theAuthorizationheader without validation,$request_filename,$fastcgi_script_nameand$fastcgi_path_info, which are built from$uri,$ssl_server_name, the TLS server name the client sent, which NGINX copies without validation.
Why this is a problem
If an attacker can inject \r\n into a header value, they can create additional headers or even influence the response body. At a minimum this is a cache poisoning and security header bypass risk, and in the worst case it becomes a response splitting attack against downstream clients.
Bad configuration
regex capture in a response header
# $action comes from a regex capture and is inserted into a response header
location ~ /v1/((?<action>[^.]*)\.json)?$ {
add_header X-Action $action;
}
If the capture allows newlines (directly, or via normalization/decoding elsewhere), an attacker can turn one header into many. For example:
GET /v1/see%20below%0d%0ax-crlf-header:injected.json HTTP/1.0
Host: localhost
HTTP/1.1 200 OK
Server: nginx/1.11.10
Date: Mon, 13 Mar 2017 21:21:29 GMT
Content-Type: application/octet-stream
Content-Length: 2
Connection: close
X-Action: see below
x-crlf-header:injected
OK
remote_user forwarded to the upstream
location / {
proxy_set_header X-User $remote_user;
proxy_pass http://backend;
}
$remote_user is the part before the first : of the base64-decoded Authorization: Basic header, and NGINX sets it for every request that carries that header, authenticated or not. Base64 can encode any byte, so a client can put \r\n into the user name and add headers of its own to the request that reaches the backend.
Better configuration
-
Prefer safer variables (for example
$request_uriover$uriwhen you need the raw input). -
Constrain captures so they cannot contain whitespace or control characters:
# Disallow slashes and whitespace in the capture
location ~ ^/some/(?<action>[^/\s]+)$ {
add_header X-Action $action;
}
-
If you must reflect client input, validate it first and keep the allowed character set tight.
-
Only forward
$remote_userfrom a scope where Basic authentication is actually enforced:
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/htpasswd;
proxy_set_header X-User $remote_user;
proxy_pass http://backend;
}
Additional notes
$remote_useris not reported whenauth_basic(notoff) andauth_basic_user_fileare both in effect for the directive andsatisfyisall(the default). The user name then has to match an entry in the password file before the request is proxied or answered. Withsatisfy any, clients that pass another access check skip the password check, so the finding stays; withoutauth_basic_user_filethe module does not enforce anything.$request(the request line) and$content_type(the requestContent-Typeheader) are client-controlled but cannot contain line breaks, so they are not reported here.