fix: match parse_field header names only at line starts#95
Conversation
155e29d to
a09277c
Compare
Send a request whose Referer value contains the substring "X-Forwarded-For: <ip>" but no real X-Forwarded-For header. With the pre-fix parse_field (strcasestr over the whole buffer) the smuggled value was logged as the client IP; with the fix the real client IP is logged.
parse_field used strcasestr against the entire request buffer, so any substring matching the header name was accepted — including bytes from the request-target or another header's value. This let any client smuggle Authorization, Range, X-Forwarded-For, Host, If-Modified-Since, etc., into log entries and request handling (notably enabling X-Forwarded-For spoofing through the trusted-proxy path). Walk header lines and only match the field at the start of a line.
a09277c to
b8680c5
Compare
|
I reordered the commits and force-pushed them, so you can checkout the one with the test but without the fix, and see it failing. |
|
Unless I'm mistaken the whole thing can be simply solved by changing Line 1785 in 7b9cafa if (pos == NULL || (pos != conn->request && pos[-1] != '\n'))
|
When the target field name appears in a prior header's value and the real header follows, a fix based on a single strcasestr call with a pos[-1] != '\n' guard returns NULL and drops the real header. The line-walking approach handles this correctly.
|
Thanks for the suggestion, @g-rden! The check does correctly reject the smuggled case, but there's a subtlety: if the field name happens to appear as a substring inside a prior header's value and a real header with that name also follows, For example, if a request contains both |
|
What if the string for the field we compare the request with already contains a leading '\n'. There would be no need for any extra testing. The request fields that are passed to the function always follow a newline anyway IFAIK. It would be as simple as: But I night have missed something |
Fixes #94.
Summary
parse_field()usedstrcasestrover the whole request buffer, so any substring matching a header name was accepted — including bytes inside the request-target or another header's value. This let clients smuggleAuthorization,Range,X-Forwarded-For,Host,If-Modified-Since, etc.--trusted-ip, a client whose connection matches the trusted proxy address could spoof its logged IP by hidingX-Forwarded-For: <ip>inside a header it controls (e.g.Referer), without ever sending a realX-Forwarded-For.strncasecmpafter each\n). The request line itself never matches a header name.Repro (pre-fix, server started with
--trusted-ip 127.0.0.1, client on 127.0.0.1):logs
99.99.99.99 - - ... "GET / ..." 200 ... "X-Forwarded-For: 99.99.99.99" "". Post-fix, real client IP is logged.Test plan
test_xff_smuggled_in_referertodevel/test_log_xff.py. Fails on7b9cafa(vulnerable), passes after the fix.test_log_xff.pycases still pass.--trusted-ip 127.0.0.1for both the smuggling attempt and a legitimateX-Forwarded-For: 1.2.3.4header.