$ cat /posts/tve-65-openresty-lua-nginx-module-ngx.req.raw_header.txt |=------=[ TVE-65: Openresty/lua-nginx-module: ngx.req.raw_header() improper handling of HTTP request / tr3e ]=------=| --[ 1 - Summary There is a vulnerability in the method ngx.req.raw_header() of lua-nginx-module. ngx.req.raw_header(), implemented by C function ngx_http_lua_ngx_req_raw_header(), returns the o riginal raw HTTP protocol header received by the Nginx server. The issue results from the improper handling of the original HTTP header. An attacker can levera ge this vulnerability to bypass certain security products, perform certain types of injections, or contaminate logs under certain circumstances. --[ 2 - Step to Produce The essential part of nginx.conf is as follows: * nginx * ------------------------------------------------------------------------------------------------ server { listen 80; server_name localhost; location / { content_by_lua_block { ngx.say(ngx.req.raw_header()) } } } ------------------------------------------------------------------------------------------------ The above config basically returns the raw content of the request header sent by the client. But it doesn't work for the following request. | request | response | |----------------------------------|-------------------------| | GET / HTTP/1.1 | GET / HTTP/1.1 | | Accept | Accept:Arbitrary-Header | | Arbitrary-Header:Arbitrary-Value | Arbitrary-Value:Host | | Host:localhost | localhost:Connection | | Connection:close | close | | | | |----------------------------------|-------------------------| | GET / HTTP/1.1 | GET / HTTP/1.1 | | Accept: */*[space] | Accept: */* | | Host: localhost | | | Header: Value | Host: localhost | | | Header: Value | | | | |----------------------------------|-------------------------| python script to reproduce the request: * Shell * ------------------------------------------------------------------------------------------------ $ python -c 'print( "GET / HTTP/1.1\n" "Accept\n" "Arbitrary-Header:Arbitrary-Value\n" "Host:localhost\n" "Connection:close\n" , end="\n")' | nc localhost 80 $ python3 -c 'print( "GET / HTTP/1.1\r\n" "Accept: */* \r\n" "Host: localhost\r\n" "Header: Value\r\n" , end="\r\n")' | nc localhost 80 ------------------------------------------------------------------------------------------------ --[ 3 - Root Cause Analysis In nginx, the buffer of the original HTTP request header is reused to construct a header list. A nd the original HTTP request header is overwritten with NULL values. (See ngx_http_request.c#145 8 and ngx_http_request.c#L1462) The position to be overwritten with NULL is determined by r->header_name_end and r->header_end. And there are multiple ways to mark header_name_end and header_end. (See ngx_http_parse.c#L974 a nd ngx_http_parse.c#L1039 ...) In openresty, they try to restore the original HTTP request header from the tainted buffer, whic h works in most cases. However, they miss some corner cases: [1] nginx supports request headers with only header name (without colon) [2] nginx marks header_end before trailing spaces [3] ... The way they restore HTTP request header cannot handle these cases. Maybe they need a different way to restore HTTP request header like nginx does, or use more memory to store the original HTT P request header.