From f8cda02268e9f42d580b9ec2313f06563fbcbcd5 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Sun, 13 Aug 2023 08:27:35 +0900 Subject: [PATCH] Fix vulnerability: restrict bitstring modifiers --- CHANGELOG.md | 4 ++ lib/dune/parser/sanitizer.ex | 72 ++++++++++++++++++++++ test/dune_string_test.exs | 114 +++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33351ad..99708f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Dev +### Bug fixes + +- Fix vulnerability allowing an attacker to crash the VM using bitstrings + ## v0.3.2 (2023-08-12) ### Enhancements diff --git a/lib/dune/parser/sanitizer.ex b/lib/dune/parser/sanitizer.ex index 6912bee..8050ae3 100644 --- a/lib/dune/parser/sanitizer.ex +++ b/lib/dune/parser/sanitizer.ex @@ -66,6 +66,16 @@ defmodule Dune.Parser.Sanitizer do message = "dune parsing error: failed to safe parse\n #{Macro.to_string(ast)}" new_failure(:parsing, message, unsafe.atom_mapping) + {:bin_modifier_restricted, ast} -> + message = + "** (DuneRestrictedError) bitstring modifier is restricted:\n #{Macro.to_string(ast)}" + + new_failure(:restricted, message, unsafe.atom_mapping) + + {:bin_modifier_size, max_size} -> + message = "** (DuneRestrictedError) size modifiers above #{max_size} are restricted" + new_failure(:restricted, message, unsafe.atom_mapping) + {:exception, error} -> message = Exception.format(:error, error) new_failure(:exception, message, unsafe.atom_mapping) @@ -433,6 +443,19 @@ defmodule Dune.Parser.Sanitizer do sanitize_capture(ast, env) end + defp do_sanitize({:<<>>, meta, args}, env) do + sanitized_args = + Enum.map(args, fn + {:"::", meta, [expr, modifier]} -> + {:"::", meta, [do_sanitize(expr, env), check_bin_modifier(modifier)]} + + arg -> + do_sanitize(arg, env) + end) + + {:<<>>, meta, sanitized_args} + end + defp do_sanitize({{:., _, [left, right]}, ctx, args} = raw, env) when is_atom(right) and is_list(args) do case left do @@ -696,6 +719,55 @@ defmodule Dune.Parser.Sanitizer do put_elem(raw, 2, safe_args) end + @max_segment_size 256 + @binary_modifiers [:binary, :bytes] + @allowed_modifiers [:integer, :float, :bits, :bitstring, :utf8, :utf16, :utf32] ++ + [:signed, :unsigned, :little, :big, :native] + + defp check_bin_modifier(modifier) do + {size, unit} = check_bin_modifier_size(modifier, 8, nil) + unit = unit || 1 + + if size * unit > @max_segment_size do + throw({:bin_modifier_size, @max_segment_size}) + end + + modifier + end + + defp check_bin_modifier_size({:-, _, [left, right]}, size, unit) do + {size, unit} = check_bin_modifier_size(left, size, unit) + check_bin_modifier_size(right, size, unit) + end + + defp check_bin_modifier_size(modifier, size, unit) do + case modifier do + new_size when is_integer(new_size) -> + {new_size, unit} + + {:size, _, [new_size]} when is_integer(new_size) -> + {new_size, unit} + + {:unit, _, [new_unit]} when is_integer(new_unit) -> + {size, new_unit} + + {:*, _, [new_size, new_unit]} when is_integer(new_size) and is_integer(new_unit) -> + {new_size, new_unit} + + {:size, _, [{:^, _, [{var, _, ctx}]}]} when is_atom(var) and is_atom(ctx) -> + {size, unit} + + {atom, _, ctx} when atom in @binary_modifiers and is_atom(ctx) -> + {size, unit || 8} + + {atom, _, ctx} when atom in @allowed_modifiers and is_atom(ctx) -> + {size, unit} + + other -> + throw({:bin_modifier_restricted, other}) + end + end + defp env_variable do Macro.var(@env_variable_name, nil) end diff --git a/test/dune_string_test.exs b/test/dune_string_test.exs index a16cb3f..7ec6521 100644 --- a/test/dune_string_test.exs +++ b/test/dune_string_test.exs @@ -141,6 +141,70 @@ defmodule DuneStringTest do } = ~E'~w[#{String.downcase("FOO")} bar baz]a' end + @tag :lts_only + test "bitstring modifiers" do + assert %Success{ + value: <<3>>, + inspected: "<<3>>" + } = ~E'<<3>>' + + assert %Success{ + value: <<3::4>>, + inspected: "<<3::size(4)>>" + } = ~E'<<3::4>>' + + assert %Success{ + value: "ߧ", + inspected: ~S'"ߧ"' + } = ~E'<<2023::utf8>>' + + assert %Success{ + value: 12520, + inspected: "12520" + } = ~E'<> = "ヨ"; c' + + assert %Success{ + value: <<3::4>>, + inspected: "<<3::size(4)>>" + } = ~E'<<3::size(4)>>' + + assert %Success{ + value: 6_382_179, + inspected: "6382179" + } = ~E'<> = "abc"; c' + + assert %Success{ + value: <<2, 3>>, + inspected: "<<2, 3>>" + } = ~E'<<_::binary-size(2), rest::binary>> = <<0, 1, 2, 3>>; rest' + + assert %Success{ + value: <<0, 0, 0, 1>>, + inspected: "<<0, 0, 0, 1>>" + } = ~E''' + x = 1 + <> + ''' + + assert %Success{ + value: <<0, 0, 0, 1>>, + inspected: "<<0, 0, 0, 1>>" + } = ~E''' + x = 1 + <> + ''' + + assert %Success{ + value: {"Frank", "Walrus"}, + inspected: ~S'{"Frank", "Walrus"}' + } = ~E''' + name_size = 5 + <> = <<"Frank the Walrus">> + {name, species} + {"Frank", "Walrus"} + ''' + end + test "binary comprehensions" do assert %Success{ value: [{213, 45, 132}, {64, 76, 32}], @@ -672,6 +736,56 @@ defmodule DuneStringTest do message: "** (DuneRestrictedError) function __ENV__/0 is restricted" } = ~E'__ENV__.requires' end + + test "bitstring modifiers" do + assert %Failure{ + type: :restricted, + message: "** (DuneRestrictedError) size modifiers above 256 are restricted" + } = ~E'<<0::123456789123456789>>' + + assert %Failure{ + type: :restricted, + message: "** (DuneRestrictedError) size modifiers above 256 are restricted" + } = ~E'<<0::size(257)>>' + + assert %Failure{ + type: :restricted, + message: "** (DuneRestrictedError) size modifiers above 256 are restricted" + } = ~E'<<0::256*2>>' + + assert %Failure{ + type: :restricted, + message: "** (DuneRestrictedError) size modifiers above 256 are restricted" + } = ~E'<<0::integer-size(257)>>' + + assert %Failure{ + type: :restricted, + message: "** (DuneRestrictedError) size modifiers above 256 are restricted" + } = ~E'<<0::integer-size(256)-unit(2)>>' + + assert %Failure{ + type: :restricted, + message: + "** (DuneRestrictedError) bitstring modifier is restricted:\n size(x)" + } = ~E''' + x = 123456789123456789 + <<0::size(x)>> + ''' + + assert %Failure{ + message: + "** (DuneRestrictedError) bitstring modifier is restricted:\n unquote(1)" + } = ~E'<<1::unquote(1)>>' + + assert %Failure{ + message: + "** (DuneRestrictedError) bitstring modifier is restricted:\n unquote(1)" + } = ~E'<<1::integer-unquote(1)>>' + + assert %Failure{ + message: "** (DuneRestrictedError) function unquote/1 is restricted" + } = ~E'<>' + end end describe "process restrictions" do