From 3000a399cbbe4e05fcdfb0e036067800a85d914f Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Mon, 19 Jan 2026 17:18:20 +0100 Subject: [PATCH] fix: sanitize header values to prevent HTTP header injection (#506) Header values containing CR or LF characters could be used for HTTP header injection attacks. This fix strips CR and LF characters from header values during serialization in to_iolist/1. The sanitization is also applied to parameter values in Content-Type and similar headers with parameters. Fixes #506 --- src/hackney_headers.erl | 20 ++++++++++++++++---- test/hackney_headers_test.erl | 30 +++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/hackney_headers.erl b/src/hackney_headers.erl index 74a6ae93..62583c78 100644 --- a/src/hackney_headers.erl +++ b/src/hackney_headers.erl @@ -179,6 +179,7 @@ to_list(Headers) -> lists:reverse(Result). %% @doc convert headers to an iolist. Useful to send them over the wire. +%% Header values are sanitized to prevent HTTP header injection (issue #506). -spec to_iolist(headers()) -> iolist(). to_iolist(Headers) -> L = fold( @@ -186,11 +187,15 @@ to_iolist(Headers) -> case Value0 of {Value, Params} -> [[ - hackney_bstr:to_binary(Key),": ", hackney_bstr:to_binary(Value), + sanitize_header_value(hackney_bstr:to_binary(Key)),": ", + sanitize_header_value(hackney_bstr:to_binary(Value)), params_to_iolist(Params, []), "\r\n" ] | L1]; _ -> - [[hackney_bstr:to_binary(Key),": ", hackney_bstr:to_binary(Value0), "\r\n"] | L1] + [[ + sanitize_header_value(hackney_bstr:to_binary(Key)),": ", + sanitize_header_value(hackney_bstr:to_binary(Value0)), "\r\n" + ] | L1] end end, [], @@ -198,11 +203,18 @@ to_iolist(Headers) -> ), lists:reverse(["\r\n" | L ]). +%% @doc Sanitize header value by removing CR and LF characters. +%% This prevents HTTP header injection attacks (CVE-like vulnerability). +-spec sanitize_header_value(binary()) -> binary(). +sanitize_header_value(Value) when is_binary(Value) -> + << <> || <> <= Value, C =/= $\r, C =/= $\n >>. + params_to_iolist([{K, V} | Rest], List) -> - List2 = [[";", hackney_bstr:to_binary(K), "=", hackney_bstr:to_binary(V)] | List], + List2 = [[";", sanitize_header_value(hackney_bstr:to_binary(K)), "=", + sanitize_header_value(hackney_bstr:to_binary(V))] | List], params_to_iolist(Rest, List2); params_to_iolist([K | Rest], List) -> - List2 = [[";", hackney_bstr:to_binary(K)] | List], + List2 = [[";", sanitize_header_value(hackney_bstr:to_binary(K))] | List], params_to_iolist(Rest, List2); params_to_iolist([], List) -> lists:reverse(List). diff --git a/test/hackney_headers_test.erl b/test/hackney_headers_test.erl index a6ccdb8e..adfb04b5 100644 --- a/test/hackney_headers_test.erl +++ b/test/hackney_headers_test.erl @@ -92,9 +92,37 @@ lookup_test() -> HList = [{<<"a">>, <<"1">>}, {<<"x-a">>, <<"a, b,c">>}, {<<"X-a">>, <<"e,f, g">>}], - + Headers = hackney_headers:from_list(HList), ?assertEqual([{<<"a">>, <<"1">>}], hackney_headers:lookup(<<"a">>, Headers)), ?assertEqual([{<<"x-a">>, <<"a, b,c">>}, {<<"X-a">>, <<"e,f, g">>}], hackney_headers:lookup(<<"x-a">>, Headers)). + +%% Test that newlines in header values are sanitized (issue #506) +%% This prevents HTTP header injection attacks +header_injection_sanitization_test() -> + %% Header with newline in value - should be stripped + HList1 = [{<<"Custom-Header">>, <<"Value\n">>}], + Headers1 = hackney_headers:from_list(HList1), + Binary1 = hackney_headers:to_binary(Headers1), + ?assertEqual(<<"Custom-Header: Value\r\n\r\n">>, Binary1), + + %% Header with CR+LF injection attempt - should be stripped + HList2 = [{<<"Custom-Header">>, <<"Value\r\nInjected-Header: malicious">>}], + Headers2 = hackney_headers:from_list(HList2), + Binary2 = hackney_headers:to_binary(Headers2), + ?assertEqual(<<"Custom-Header: ValueInjected-Header: malicious\r\n\r\n">>, Binary2), + + %% Header with only CR - should be stripped + HList3 = [{<<"Custom-Header">>, <<"Value\rMore">>}], + Headers3 = hackney_headers:from_list(HList3), + Binary3 = hackney_headers:to_binary(Headers3), + ?assertEqual(<<"Custom-Header: ValueMore\r\n\r\n">>, Binary3), + + %% Multiple headers, one with injection attempt + HList4 = [{<<"Normal-Header">>, <<"normal">>}, + {<<"Bad-Header">>, <<"value\r\n\r\nHTTP/1.1 200 OK">>}], + Headers4 = hackney_headers:from_list(HList4), + Binary4 = hackney_headers:to_binary(Headers4), + ?assertEqual(<<"Normal-Header: normal\r\nBad-Header: valueHTTP/1.1 200 OK\r\n\r\n">>, Binary4). \ No newline at end of file