-
-
Notifications
You must be signed in to change notification settings - Fork 44
fix: reject request headers that contain CR or LF #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import pytest | ||
| from common import pg_collect_response, pg_http_request | ||
| from test_http_timeout import wait_for_responses | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "header, reported_name", | ||
| [ | ||
| ('{"X-Test": "value\\n"}', "X-Test"), | ||
| ('{"X-Test": "value\\r"}', "X-Test"), | ||
| ('{"X-Test": "value\\r\\nInjected: yes"}', "X-Test"), | ||
| ('{"X-Te\\nst": "value"}', "X-Te"), | ||
| ('{"\\r\\nInjected": "yes"}', ""), | ||
| ], | ||
| ) | ||
| def test_headers_with_cr_or_lf_are_rejected(conn, header, reported_name): | ||
| """A header containing CR or LF is not sent and gets an ERROR response naming the header""" | ||
|
|
||
| request_id = pg_http_request( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @utkarash2991 I feel like the validation/error should be happening in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Agree, we need to get a sense of urgency. Looking at the below quote from the docs I'm not seeing a huge vulnerability here. My understanding is that any vulnerability would only happen if the server improperly parsed headers as well. Defense in depth is important but I don't think that would be pg_net's fault.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty confident at this point that this should be a low priority fix. Also IMO a blanket rejection is not the correct approach. Take a look at my repro in the original issue along with corresponding behavior of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not for security but usability, although I'm not ruling out the defence in depth security aspects. It's not urgent in that not many people have reported it. Blanket reject is the correct approach and
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And curl is trying to fix this as well: curl/curl#22309
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO we should merge it, not because it's urgent but because this is a small focussed fix and it's not much rework to later design a better interface (via domain type) as suggested by @steve-chavez above. |
||
| conn, | ||
| "select net.http_get(url := 'http://localhost:8080/headers', headers := %s::jsonb)", | ||
| (header,), | ||
| ) | ||
|
|
||
| (status_code, error_msg, timed_out) = wait_for_responses(conn, [request_id])[ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another question: should we be rejecting these requests? Currently we are straight up rejecting to execute them but pgsql-http allows them to go through and forms the header value with the newline character in it. I suspect their design is correct and ours is wrong here but happy to be wrong. Is there no potential use value to having a newline in a json value? pgsql-http example call pgsql http forms header with newline example python web server to print headers
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After further analysis curl does not reject these types of requests either. Given that I don't think we should either.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We definitely should reject such requests. pgsql-http and curl are in the wrong here, not us trying to steer users clear of building requests which will be incorrect 100% of the time. Adding this to docs doesn't cut it because users do not always read them and it's far better to prevent a problem than to debug it. Docs are not executable. |
||
| request_id | ||
| ] | ||
|
|
||
| assert status_code is None | ||
| assert timed_out is False | ||
| assert ( | ||
| error_msg == f'header "{reported_name}" contains a carriage return or line feed' | ||
| ) | ||
| assert "value" not in error_msg | ||
| assert "Injected" not in error_msg | ||
|
|
||
|
|
||
| def test_headers_without_cr_or_lf_are_sent(conn): | ||
| """Ordinary headers still reach the server""" | ||
|
|
||
| request_id = pg_http_request( | ||
| conn, | ||
| """select net.http_get( | ||
| url := 'http://localhost:8080/headers', | ||
| headers := '{"X-Test": "plain value", "accept": "application/json"}'::jsonb | ||
| )""", | ||
| ) | ||
|
|
||
| response = pg_collect_response(conn, request_id) | ||
|
|
||
| assert response["status"] == "SUCCESS" | ||
| assert "X-Test" in response["body"] | ||
|
|
||
|
|
||
| def test_rejected_header_does_not_affect_the_rest_of_the_batch(conn): | ||
| """One bad header in a batch gets an ERROR row, the other requests are sent normally""" | ||
|
|
||
| bad = pg_http_request( | ||
| conn, | ||
| """select net.http_get(url := 'http://localhost:8080/headers', headers := '{"X-Test": "a\\nb"}'::jsonb)""", | ||
| ) | ||
| get = pg_http_request( | ||
| conn, "select net.http_get(url := 'http://localhost:8080/headers')" | ||
| ) | ||
| post = pg_http_request( | ||
| conn, | ||
| "select net.http_post(url := 'http://localhost:8080/anything', body := '{}'::jsonb)", | ||
| ) | ||
|
|
||
| responses = wait_for_responses(conn, [bad, get, post]) | ||
|
|
||
| assert responses[bad][0] is None | ||
| assert ( | ||
| responses[bad][1] == 'header "X-Test" contains a carriage return or line feed' | ||
| ) | ||
| assert responses[get][0] == 200 | ||
| assert responses[post][0] == 200 | ||
|
|
||
|
|
||
| def test_direct_insert_with_cr_or_lf_header_is_rejected(conn): | ||
| """Rows inserted straight into the queue go through the same check""" | ||
|
|
||
| request_id = pg_http_request( | ||
| conn, | ||
| """insert into net.http_request_queue(method, url, headers, timeout_milliseconds) | ||
| values ('GET', 'http://localhost:8080/headers', '{"X-Test": "a\\rb"}'::jsonb, 5000) | ||
| returning id""", | ||
| ) | ||
| conn.execute("select net.wake()") | ||
| conn.commit() | ||
|
|
||
| (status_code, error_msg, _) = wait_for_responses(conn, [request_id])[request_id] | ||
|
|
||
| assert status_code is None | ||
| assert error_msg == 'header "X-Test" contains a carriage return or line feed' | ||
|
|
||
| follow_up = pg_http_request( | ||
| conn, "select net.http_get(url := 'http://localhost:8080/headers')" | ||
| ) | ||
| assert wait_for_responses(conn, [follow_up])[follow_up][0] == 200 | ||
|
|
||
|
|
||
| def test_timeout_rejection_is_reported_before_the_header_one(conn): | ||
| """A request with both a bad timeout and a bad header gets the timeout message""" | ||
|
|
||
| request_id = pg_http_request( | ||
| conn, | ||
| """select net.http_get( | ||
| url := 'http://localhost:8080/headers', | ||
| headers := '{"X-Test": "a\\nb"}'::jsonb, | ||
| timeout_milliseconds := 0 | ||
| )""", | ||
| ) | ||
|
|
||
| (_, error_msg, _) = wait_for_responses(conn, [request_id])[request_id] | ||
|
|
||
| assert ( | ||
| error_msg | ||
| == "timeout_milliseconds must be between 1 and 600000 (pg_net.max_timeout_ms), got 0" | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can reword it in terms of correctness rather than security by removing the word "smuggle" and saying that this leads to creation of malformed requests.