Skip to content

Commit ec5e09b

Browse files
committed
Add non_executable option to binary_to_term/2
The option rejects any function reference (anonymous or external) from being serialized. The goal is to avoid executable code from being smuggled to data structures that lazily stores computations. For example, imagine you have a module created `lazy_lists` that accumulates operations which are executed at once. If not careful, `binary_to_term/2` could deserialize a custom lazy list which, combined with `erl_eval`, may execute any code. Most web functionality in Elixir already does a post-pass on binary_to_term/2 to reject functions, but this allows us to do so much more efficiently.
1 parent eab8e26 commit ec5e09b

6 files changed

Lines changed: 73 additions & 7 deletions

File tree

erts/emulator/beam/atom.names

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ atom normal_exit
516516
atom nosuspend
517517
atom no_fail
518518
atom no_float
519+
atom non_executable
519520
atom no_integer
520521
atom no_network
521522
atom no_start_optimize

erts/emulator/beam/external.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1317,7 +1317,7 @@ Eterm erts_decode_ext(ErtsHeapFactory* factory, const byte **ext, Uint32 flags)
13171317
erts_factory_undo(factory);
13181318
return THE_NON_VALUE;
13191319
}
1320-
ASSERT(!(flags & ~ERTS_DIST_EXT_BTT_SAFE));
1320+
ASSERT(!(flags & ~(ERTS_DIST_EXT_BTT_SAFE|ERTS_DIST_EXT_BTT_NON_EXECUTABLE)));
13211321
ep = dec_term(NULL, factory, ep, &obj, NULL, flags);
13221322
if (!ep) {
13231323
return THE_NON_VALUE;
@@ -2154,6 +2154,9 @@ BIF_RETTYPE binary_to_term_2(BIF_ALIST_2)
21542154
if (opt == am_safe) {
21552155
ctx.flags |= ERTS_DIST_EXT_BTT_SAFE;
21562156
}
2157+
else if (opt == am_non_executable) {
2158+
ctx.flags |= ERTS_DIST_EXT_BTT_NON_EXECUTABLE;
2159+
}
21572160
else if (opt == am_used) {
21582161
ctx.used_bytes = 1;
21592162
}
@@ -4990,6 +4993,9 @@ dec_term(ErtsDistExternal *edep,
49904993
Eterm temp;
49914994
Sint arity;
49924995

4996+
if (flags & ERTS_DIST_EXT_BTT_NON_EXECUTABLE) {
4997+
goto error;
4998+
}
49934999
if ((ep = dec_atom(edep, ep, &mod, flags)) == NULL) {
49945000
goto error;
49955001
}
@@ -5091,6 +5097,10 @@ dec_term(ErtsDistExternal *edep,
50915097
int i;
50925098
Eterm temp;
50935099

5100+
if (flags & ERTS_DIST_EXT_BTT_NON_EXECUTABLE) {
5101+
goto error;
5102+
}
5103+
50945104
ep += 4; /* Skip total size in bytes */
50955105
arity = *ep++;
50965106
uniq = ep;

erts/emulator/beam/external.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,11 @@ typedef struct {
129129
* They are used to indicate various bits of state necessary to decode binaries
130130
* in a variety of scenarios.
131131
*/
132-
#define ERTS_DIST_EXT_DFLAG_HDR ((Uint32) 0x1)
133-
#define ERTS_DIST_EXT_ATOM_TRANS_TAB ((Uint32) 0x2)
134-
#define ERTS_DIST_EXT_BTT_SAFE ((Uint32) 0x4)
135-
#define ERTS_DIST_EXT_INTERNAL_NC ((Uint32) 0x8)
132+
#define ERTS_DIST_EXT_DFLAG_HDR ((Uint32) 0x1)
133+
#define ERTS_DIST_EXT_ATOM_TRANS_TAB ((Uint32) 0x2)
134+
#define ERTS_DIST_EXT_BTT_SAFE ((Uint32) 0x4)
135+
#define ERTS_DIST_EXT_INTERNAL_NC ((Uint32) 0x8)
136+
#define ERTS_DIST_EXT_BTT_NON_EXECUTABLE ((Uint32) 0x10)
136137

137138
#define ERTS_DIST_CON_ID_MASK ((Uint32) 0x00ffffff)
138139

erts/emulator/test/binary_SUITE.erl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
unsorted_map_in_map/1,
6969
bad_term_to_binary/1,
7070
bad_binary_to_term_2/1,safe_binary_to_term2/1,
71+
non_executable_binary_to_term2/1,
7172
bad_binary_to_term/1, bad_terms/1, more_bad_terms/1,
7273
big_binary_to_term/1,
7374
otp_5484/1,otp_5933/1,
@@ -99,6 +100,7 @@ all() ->
99100
b2t_used_big, t2b_deterministic,
100101
t2b_minor_version,
101102
bad_binary_to_term_2, safe_binary_to_term2,
103+
non_executable_binary_to_term2,
102104
bad_binary_to_term, bad_terms, t_hash, bad_size,
103105
big_binary_to_term,
104106
sub_bin_copy, bad_term_to_binary, t2b_system_limit,
@@ -1210,6 +1212,30 @@ safe_binary_to_term2(Config) when is_list(Config) ->
12101212
bad_bin_to_term(BadExtFun, [safe]),
12111213
ok.
12121214

1215+
%% Test the non_executable option for binary_to_term/2
1216+
non_executable_binary_to_term2(Config) when is_list(Config) ->
1217+
hello = binary_to_term_stress(term_to_binary(hello), [non_executable]),
1218+
{1,[2,3]} = binary_to_term_stress(term_to_binary({1,[2,3]}), [non_executable]),
1219+
1220+
%% External fun references
1221+
ExtFun = term_to_binary(fun erlang:length/1),
1222+
bad_bin_to_term(ExtFun, [non_executable]),
1223+
1224+
%% Anonymous funs
1225+
Fun0 = term_to_binary(fun() -> ok end),
1226+
bad_bin_to_term(Fun0, [non_executable]),
1227+
Fun1 = term_to_binary(fun(X) -> X end),
1228+
bad_bin_to_term(Fun1, [non_executable]),
1229+
1230+
%% A fun nested inside another term
1231+
Nested = term_to_binary({wrapper, [fun() -> ok end]}),
1232+
bad_bin_to_term(Nested, [non_executable]),
1233+
1234+
%% Composing with safe
1235+
bad_bin_to_term(ExtFun, [safe, non_executable]),
1236+
hello = binary_to_term_stress(term_to_binary(hello), [safe, non_executable]),
1237+
ok.
1238+
12131239
%% OTP-18306 Decode binary/bitstring with size >= 2Gbyte
12141240
big_binary_to_term(Config) ->
12151241
run_when_enough_resources(

erts/preloaded/src/erlang.erl

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,26 @@ The allowed options are:
13601360
tjenixen
13611361
```
13621362

1363+
- **`non_executable`** - Use this option when receiving binaries from a source
1364+
that should not be allowed to deliver executable terms.
1365+
1366+
When enabled, decoding fails with a `badarg` error if the binary contains
1367+
any function references, including anonymous funs (`fun () -> ... end`) and
1368+
external function references (`fun Module:Name/Arity`). This is useful when
1369+
the consumer never expects to receive a function from the producer and
1370+
wishes to defend against attacks that would otherwise smuggle a function
1371+
through the encoded term.
1372+
1373+
The `non_executable` option can be combined with `safe`.
1374+
1375+
## Examples
1376+
1377+
```erlang
1378+
1> Bin = term_to_binary(fun erlang:length/1).
1379+
2> binary_to_term(Bin, [non_executable]).
1380+
** exception error: bad argument
1381+
```
1382+
13631383
- **`used`** - Changes the return value to `{Term, Used}` where `Used` is the
13641384
number of bytes actually read from `Binary`.
13651385

@@ -1374,15 +1394,16 @@ The allowed options are:
13741394
{<<131,119,5,104,101,108,108,111>>, <<"world">>}
13751395
```
13761396

1377-
Failure: `badarg` if `safe` is specified and unsafe data is decoded.
1397+
Failure: `badarg` if `safe` is specified and unsafe data is decoded, or if
1398+
`non_executable` is specified and the binary contains a function reference.
13781399

13791400
See also `term_to_binary/1`, `binary_to_term/1`, and `list_to_existing_atom/1`.
13801401
""".
13811402
-doc(#{since => <<"OTP R13B04">>}).
13821403
-doc #{ category => terms }.
13831404
-spec binary_to_term(Binary, Opts) -> term() | {term(), Used} when
13841405
Binary :: ext_binary(),
1385-
Opt :: safe | used,
1406+
Opt :: safe | non_executable | used,
13861407
Opts :: [Opt],
13871408
Used :: pos_integer().
13881409
binary_to_term(_Binary, _Opts) ->

system/doc/design_principles/secure_coding.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,13 @@ One issue with this being the potential for atom exhaustion, but more
10261026
importantly you could potentially end up with a `mnesia` table containing
10271027
harmful data ([`CWE-502`]). Other examples are `m:dets` and `m:disk_log`.
10281028

1029+
The `erlang:binary_to_term/2` function can be used to deserialize data
1030+
and must also only be used with trusted data. It is recommended to use
1031+
both the `safe` flag, to avoid atom exhaustion ([`DSG-003`]), and
1032+
the `non_executable` flag, to raise when deserializing external or
1033+
anonymous functions. The latter protects untrusted data from potentially
1034+
escalating into remote code execution.
1035+
10291036
JSON is an example of a better format to use when communicating with untrusted
10301037
entities. Erlang/OTP provides the `m:json` module for JSON encoding/decoding.
10311038
XML is another example of a format that can be used. The [`xmerl`] application

0 commit comments

Comments
 (0)