Skip to content

Commit a9999e4

Browse files
committed
Fix unsafe deserialization of Erlang terms in API responses
1 parent b797d12 commit a9999e4

File tree

2 files changed

+101
-2
lines changed

2 files changed

+101
-2
lines changed

src/mix_hex_api.erl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
%% Vendored from hex_core v0.12.0 (1cdf3eb), do not edit manually
1+
%% Vendored from hex_core MANUALLY!, do not edit manually
22

33
%% @doc
44
%% Hex HTTP API
@@ -106,7 +106,12 @@ request(Config, Method, Path, Body) when is_binary(Path) and is_map(Config) ->
106106
Response =
107107
case binary:match(ContentType, ?ERL_CONTENT_TYPE) of
108108
{_, _} ->
109-
{ok, {Status, RespHeaders, binary_to_term(RespBody)}};
109+
case mix_hex_safe_binary_to_term:safe_binary_to_term(RespBody) of
110+
{ok, Term} ->
111+
{ok, {Status, RespHeaders, Term}};
112+
{error, Reason} ->
113+
{error, Reason}
114+
end;
110115
nomatch ->
111116
{ok, {Status, RespHeaders, nil}}
112117
end,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
%% Vendored from hex_core MANUALLY!, do not edit manually
2+
3+
%% @hidden
4+
%% Safe deserialization of Erlang terms from binary.
5+
%%
6+
%% This module provides a restricted version of `binary_to_term/1' that:
7+
%% - Uses the `safe' option to prevent creation of new atoms (DoS protection)
8+
%% - Validates that the term contains no executable code (RCE protection)
9+
%%
10+
%% Inspired by Plug.Crypto's non_executable_binary_to_term:
11+
%% https://github.com/elixir-plug/plug_crypto/blob/c326c3c743b18cf5f4b12735d06dd90c72dcd779/lib/plug/crypto.ex
12+
-module(mix_hex_safe_binary_to_term).
13+
14+
-export([safe_binary_to_term/1]).
15+
16+
-type unsafe_term() :: function() | port().
17+
-type error_reason() :: invalid_term | {unsafe_term, unsafe_term()}.
18+
19+
-spec safe_binary_to_term(binary()) -> {ok, term()} | {error, error_reason()}.
20+
safe_binary_to_term(Binary) when is_binary(Binary) ->
21+
try binary_to_term(Binary, [safe]) of
22+
Term ->
23+
case validate_term(Term) of
24+
ok -> {ok, Term};
25+
{error, _} = Error -> Error
26+
end
27+
catch
28+
error:badarg ->
29+
{error, invalid_term}
30+
end.
31+
32+
-spec validate_term(term()) -> ok | {error, {unsafe_term, term()}}.
33+
validate_term(Term) when is_list(Term) ->
34+
validate_list(Term);
35+
validate_term(Term) when is_tuple(Term) ->
36+
validate_tuple(Term, tuple_size(Term));
37+
validate_term(Term) when is_map(Term) ->
38+
validate_map(Term);
39+
validate_term(Term) when
40+
is_atom(Term);
41+
is_number(Term);
42+
is_bitstring(Term);
43+
is_pid(Term);
44+
is_reference(Term)
45+
->
46+
ok;
47+
validate_term(Term) ->
48+
{error, {unsafe_term, Term}}.
49+
50+
-spec validate_list(list()) -> ok | {error, {unsafe_term, term()}}.
51+
validate_list([]) ->
52+
ok;
53+
validate_list([H | T]) when is_list(T) ->
54+
case validate_term(H) of
55+
ok -> validate_list(T);
56+
Error -> Error
57+
end;
58+
validate_list([H | T]) ->
59+
%% Improper list
60+
case validate_term(H) of
61+
ok -> validate_term(T);
62+
Error -> Error
63+
end.
64+
65+
-spec validate_tuple(tuple(), non_neg_integer()) -> ok | {error, {unsafe_term, term()}}.
66+
validate_tuple(_Tuple, 0) ->
67+
ok;
68+
validate_tuple(Tuple, N) ->
69+
case validate_term(element(N, Tuple)) of
70+
ok -> validate_tuple(Tuple, N - 1);
71+
Error -> Error
72+
end.
73+
74+
-spec validate_map(map()) -> ok | {error, {unsafe_term, term()}}.
75+
validate_map(Map) ->
76+
try
77+
maps:fold(
78+
fun(Key, Value, ok) ->
79+
case validate_term(Key) of
80+
ok ->
81+
case validate_term(Value) of
82+
ok -> ok;
83+
Error -> throw(Error)
84+
end;
85+
Error ->
86+
throw(Error)
87+
end
88+
end,
89+
ok,
90+
Map
91+
)
92+
catch
93+
throw:{error, _} = Error -> Error
94+
end.

0 commit comments

Comments
 (0)