Skip to content

Commit 9d0eb82

Browse files
authored
fix: sanitize header values to prevent HTTP header injection (#506) (#812)
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
1 parent 01e97d9 commit 9d0eb82

File tree

2 files changed

+45
-5
lines changed

2 files changed

+45
-5
lines changed

src/hackney_headers.erl

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,30 +179,42 @@ to_list(Headers) ->
179179
lists:reverse(Result).
180180

181181
%% @doc convert headers to an iolist. Useful to send them over the wire.
182+
%% Header values are sanitized to prevent HTTP header injection (issue #506).
182183
-spec to_iolist(headers()) -> iolist().
183184
to_iolist(Headers) ->
184185
L = fold(
185186
fun(Key, Value0, L1) ->
186187
case Value0 of
187188
{Value, Params} ->
188189
[[
189-
hackney_bstr:to_binary(Key),": ", hackney_bstr:to_binary(Value),
190+
sanitize_header_value(hackney_bstr:to_binary(Key)),": ",
191+
sanitize_header_value(hackney_bstr:to_binary(Value)),
190192
params_to_iolist(Params, []), "\r\n"
191193
] | L1];
192194
_ ->
193-
[[hackney_bstr:to_binary(Key),": ", hackney_bstr:to_binary(Value0), "\r\n"] | L1]
195+
[[
196+
sanitize_header_value(hackney_bstr:to_binary(Key)),": ",
197+
sanitize_header_value(hackney_bstr:to_binary(Value0)), "\r\n"
198+
] | L1]
194199
end
195200
end,
196201
[],
197202
Headers
198203
),
199204
lists:reverse(["\r\n" | L ]).
200205

206+
%% @doc Sanitize header value by removing CR and LF characters.
207+
%% This prevents HTTP header injection attacks (CVE-like vulnerability).
208+
-spec sanitize_header_value(binary()) -> binary().
209+
sanitize_header_value(Value) when is_binary(Value) ->
210+
<< <<C>> || <<C>> <= Value, C =/= $\r, C =/= $\n >>.
211+
201212
params_to_iolist([{K, V} | Rest], List) ->
202-
List2 = [[";", hackney_bstr:to_binary(K), "=", hackney_bstr:to_binary(V)] | List],
213+
List2 = [[";", sanitize_header_value(hackney_bstr:to_binary(K)), "=",
214+
sanitize_header_value(hackney_bstr:to_binary(V))] | List],
203215
params_to_iolist(Rest, List2);
204216
params_to_iolist([K | Rest], List) ->
205-
List2 = [[";", hackney_bstr:to_binary(K)] | List],
217+
List2 = [[";", sanitize_header_value(hackney_bstr:to_binary(K))] | List],
206218
params_to_iolist(Rest, List2);
207219
params_to_iolist([], List) ->
208220
lists:reverse(List).

test/hackney_headers_test.erl

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,37 @@ lookup_test() ->
9292
HList = [{<<"a">>, <<"1">>},
9393
{<<"x-a">>, <<"a, b,c">>},
9494
{<<"X-a">>, <<"e,f, g">>}],
95-
95+
9696
Headers = hackney_headers:from_list(HList),
9797
?assertEqual([{<<"a">>, <<"1">>}], hackney_headers:lookup(<<"a">>, Headers)),
9898
?assertEqual([{<<"x-a">>, <<"a, b,c">>},
9999
{<<"X-a">>, <<"e,f, g">>}], hackney_headers:lookup(<<"x-a">>, Headers)).
100+
101+
%% Test that newlines in header values are sanitized (issue #506)
102+
%% This prevents HTTP header injection attacks
103+
header_injection_sanitization_test() ->
104+
%% Header with newline in value - should be stripped
105+
HList1 = [{<<"Custom-Header">>, <<"Value\n">>}],
106+
Headers1 = hackney_headers:from_list(HList1),
107+
Binary1 = hackney_headers:to_binary(Headers1),
108+
?assertEqual(<<"Custom-Header: Value\r\n\r\n">>, Binary1),
109+
110+
%% Header with CR+LF injection attempt - should be stripped
111+
HList2 = [{<<"Custom-Header">>, <<"Value\r\nInjected-Header: malicious">>}],
112+
Headers2 = hackney_headers:from_list(HList2),
113+
Binary2 = hackney_headers:to_binary(Headers2),
114+
?assertEqual(<<"Custom-Header: ValueInjected-Header: malicious\r\n\r\n">>, Binary2),
115+
116+
%% Header with only CR - should be stripped
117+
HList3 = [{<<"Custom-Header">>, <<"Value\rMore">>}],
118+
Headers3 = hackney_headers:from_list(HList3),
119+
Binary3 = hackney_headers:to_binary(Headers3),
120+
?assertEqual(<<"Custom-Header: ValueMore\r\n\r\n">>, Binary3),
121+
122+
%% Multiple headers, one with injection attempt
123+
HList4 = [{<<"Normal-Header">>, <<"normal">>},
124+
{<<"Bad-Header">>, <<"value\r\n\r\nHTTP/1.1 200 OK">>}],
125+
Headers4 = hackney_headers:from_list(HList4),
126+
Binary4 = hackney_headers:to_binary(Headers4),
127+
?assertEqual(<<"Normal-Header: normal\r\nBad-Header: valueHTTP/1.1 200 OK\r\n\r\n">>, Binary4).
100128

0 commit comments

Comments
 (0)