Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions src/hackney_headers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -179,30 +179,42 @@ 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(
fun(Key, Value0, L1) ->
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,
[],
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) ->
<< <<C>> || <<C>> <= 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).
Expand Down
30 changes: 29 additions & 1 deletion test/hackney_headers_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Loading