Skip to content

Commit 4d58acb

Browse files
Native STOMP: make the frame parser more robust
1. A negative content-length crashed the parser in `parse_known_body/4`. 2. A non-numeric content-length (e.g. "abc") crashed `integer_header/2` with a `badarg` exception 3. A content-length pointing past the NUL terminator resulted in a `badmatch`. All three are pre-auth exceptions. With these changes: 1. Negative values now return `{error, {invalid_content_length, N}}` 2. Non-numeric values are treated as absent 3. A missing NUL terminator returns `{error, missing_body_terminator}`
1 parent 411f525 commit 4d58acb

3 files changed

Lines changed: 133 additions & 5 deletions

File tree

deps/rabbitmq_stomp/src/rabbit_stomp_frame.erl

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ parse_body(Content, #ps{cmd = Cmd, hdrs = Hdrs,
364364
case Cmd of
365365
'SEND' ->
366366
case integer_header(Frame, ?HEADER_CONTENT_LENGTH, unknown) of
367+
ContentLength when is_integer(ContentLength),
368+
ContentLength < 0 ->
369+
{error, {invalid_content_length, ContentLength}};
367370
ContentLength when is_integer(ContentLength),
368371
ContentLength > MaxBodyLength ->
369372
{error, {max_body_length, ContentLength}};
@@ -403,9 +406,13 @@ parse_known_body(Content, Frame, Chunks, Remaining) ->
403406
end.
404407

405408
finish_body(Content, Frame, Chunks, Pos) ->
406-
<<Chunk:Pos/binary, 0, Rest/binary>> = Content,
407-
Body = finalize_chunk(Chunk, Chunks),
408-
{ok, Frame#stomp_frame{body_iolist_rev = Body}, Rest}.
409+
case Content of
410+
<<Chunk:Pos/binary, 0, Rest/binary>> ->
411+
Body = finalize_chunk(Chunk, Chunks),
412+
{ok, Frame#stomp_frame{body_iolist_rev = Body}, Rest};
413+
_ ->
414+
{error, missing_body_terminator}
415+
end.
409416

410417
finalize_chunk(<<>>, Chunks) -> Chunks;
411418
finalize_chunk(Chunk, Chunks) -> [Chunk | Chunks].
@@ -444,7 +451,10 @@ boolean_header(F, K, D) -> default_value(boolean_header(F, K), D).
444451

445452
integer_header(F, Key) ->
446453
case header(F, Key) of
447-
{ok, Str} -> {ok, binary_to_integer(string:trim(Str))};
454+
{ok, Str} ->
455+
try {ok, binary_to_integer(string:trim(Str))}
456+
catch error:badarg -> not_found
457+
end;
448458
not_found -> not_found
449459
end.
450460

deps/rabbitmq_stomp/test/prop_frame_SUITE.erl

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ groups() ->
2626
[
2727
prop_within_limit_succeeds,
2828
prop_over_limit_fails,
29-
prop_duplicates_do_not_exhaust_limit
29+
prop_duplicates_do_not_exhaust_limit,
30+
prop_negative_content_length_rejected,
31+
prop_valid_content_length_round_trips,
32+
prop_non_numeric_content_length_does_not_crash
3033
]}].
3134

3235
%% Any frame with up to LIMIT unique header names parses successfully.
@@ -65,6 +68,46 @@ prop_duplicates_do_not_exhaust_limit(_Config) ->
6568
end)
6669
end, [], 1000).
6770

71+
%% Any negative content-length is rejected.
72+
prop_negative_content_length_rejected(_Config) ->
73+
run_proper(
74+
fun() ->
75+
?FORALL(N, range(-10000, -1),
76+
begin
77+
{error, {invalid_content_length, N}} =
78+
parse(send_frame("content-length", integer_to_list(N), "x")),
79+
true
80+
end)
81+
end, [], 1000).
82+
83+
%% A SEND frame with a matching content-length and body always parses.
84+
prop_valid_content_length_round_trips(_Config) ->
85+
run_proper(
86+
fun() ->
87+
?FORALL(Body, binary(),
88+
begin
89+
Len = integer_to_list(byte_size(Body)),
90+
case parse(send_frame("content-length", Len, Body)) of
91+
{ok, #stomp_frame{command = 'SEND'}, _} -> true;
92+
{more, _} -> true
93+
end
94+
end)
95+
end, [], 1000).
96+
97+
%% Non-numeric content-length values must never crash the parser.
98+
prop_non_numeric_content_length_does_not_crash(_Config) ->
99+
run_proper(
100+
fun() ->
101+
?FORALL(Junk, non_numeric_bin(),
102+
begin
103+
case parse(send_frame("content-length", binary_to_list(Junk), "x")) of
104+
{ok, _, _} -> true;
105+
{more, _} -> true;
106+
{error, _} -> true
107+
end
108+
end)
109+
end, [], 1000).
110+
68111
%%-------------------------------------------------------------------
69112

70113
unique_headers(N) ->
@@ -74,5 +117,18 @@ make_frame(Headers) ->
74117
HdrStr = lists:flatten([K ++ ":" ++ V ++ "\n" || {K, V} <- Headers]),
75118
iolist_to_binary(["CONNECT\n", HdrStr, "\n\0"]).
76119

120+
send_frame(HdrName, HdrValue, Body) ->
121+
iolist_to_binary(["SEND\ndestination:/queue/t\n",
122+
HdrName, ":", HdrValue, "\n\n",
123+
Body, "\0"]).
124+
125+
%% Generator for binaries that are not valid integer strings.
126+
non_numeric_bin() ->
127+
?SUCHTHAT(Bin,
128+
?LET(Chars, list(range(0, 255)),
129+
list_to_binary(
130+
[C || C <- Chars, C =/= $\n, C =/= $\r, C =/= $:, C =/= $\\, C =/= 0])),
131+
not is_integer(catch binary_to_integer(string:trim(Bin)))).
132+
77133
parse(Bin) ->
78134
rabbit_stomp_frame:parse(Bin, rabbit_stomp_frame:initial_state()).
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
-module(unit_content_length_SUITE).
9+
10+
-include_lib("eunit/include/eunit.hrl").
11+
-include("rabbit_stomp_frame.hrl").
12+
-compile(export_all).
13+
14+
all() ->
15+
[
16+
negative_content_length_rejected,
17+
non_numeric_content_length_treated_as_unknown,
18+
zero_content_length_accepted,
19+
valid_content_length_accepted,
20+
missing_nul_terminator_rejected,
21+
content_length_with_whitespace_accepted
22+
].
23+
24+
negative_content_length_rejected(_) ->
25+
?assertMatch({error, {invalid_content_length, -1}},
26+
parse(send_frame([{"content-length", "-1"}], "hello"))).
27+
28+
non_numeric_content_length_treated_as_unknown(_) ->
29+
%% Non-numeric content-length is ignored; the parser falls back
30+
%% to scanning for the NUL terminator.
31+
{ok, Frame, _} = parse(send_frame([{"content-length", "abc"}], "hello")),
32+
?assertEqual('SEND', Frame#stomp_frame.command).
33+
34+
zero_content_length_accepted(_) ->
35+
{ok, Frame, _} = parse(send_frame([{"content-length", "0"}], "")),
36+
?assertEqual('SEND', Frame#stomp_frame.command),
37+
?assertEqual([], Frame#stomp_frame.body_iolist_rev).
38+
39+
valid_content_length_accepted(_) ->
40+
{ok, Frame, _} = parse(send_frame([{"content-length", "5"}], "hello")),
41+
?assertEqual('SEND', Frame#stomp_frame.command),
42+
?assertEqual([<<"hello">>], Frame#stomp_frame.body_iolist_rev).
43+
44+
%% When content-length is set and the byte at that position is not NUL,
45+
%% the parser must return an error instead of crashing.
46+
missing_nul_terminator_rejected(_) ->
47+
Bin = <<"SEND\ndestination:/queue/t\ncontent-length:3\n\nhelloX">>,
48+
?assertMatch({error, missing_body_terminator}, parse(Bin)).
49+
50+
content_length_with_whitespace_accepted(_) ->
51+
{ok, Frame, _} = parse(send_frame([{"content-length", " 5 "}], "hello")),
52+
?assertEqual('SEND', Frame#stomp_frame.command).
53+
54+
%%-------------------------------------------------------------------
55+
56+
send_frame(Headers, Body) ->
57+
HdrStr = lists:flatten(
58+
[K ++ ":" ++ V ++ "\n" || {K, V} <- [{"destination", "/queue/t"} | Headers]]),
59+
iolist_to_binary(["SEND\n", HdrStr, "\n", Body, "\0"]).
60+
61+
parse(Bin) ->
62+
rabbit_stomp_frame:parse(Bin, rabbit_stomp_frame:initial_state()).

0 commit comments

Comments
 (0)