Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions lib/util/safe_regex.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule Util.SafeRegex do
@moduledoc """
Bounded regex matching for parameter input format validation. Caps
pattern and value sizes; PCRE's `match_limit` short-circuits runaway
matches.
"""

@max_pattern_length 512
@max_value_length 4_096

@type match_error :: :pattern_too_long | :value_too_long | :invalid_pattern

@spec max_pattern_length() :: pos_integer()
def max_pattern_length, do: @max_pattern_length

@spec max_value_length() :: pos_integer()
def max_value_length, do: @max_value_length

@spec validate_pattern(String.t() | nil) :: :ok | {:error, match_error()}
def validate_pattern(nil), do: {:error, :invalid_pattern}
def validate_pattern(""), do: {:error, :invalid_pattern}

def validate_pattern(pattern) when is_binary(pattern) do
if byte_size(pattern) > @max_pattern_length do
{:error, :pattern_too_long}
else
case :re.compile(pattern) do
{:ok, _compiled} -> :ok
{:error, _reason} -> {:error, :invalid_pattern}
end
end
end

@spec match(String.t() | nil, String.t() | nil) ::
{:ok, boolean()} | {:error, match_error()}
def match(nil, _value), do: {:error, :invalid_pattern}
def match(_pattern, nil), do: {:ok, false}

def match(pattern, value) when is_binary(pattern) and is_binary(value) do
cond do
byte_size(pattern) > @max_pattern_length -> {:error, :pattern_too_long}
byte_size(value) > @max_value_length -> {:error, :value_too_long}
true -> run(pattern, value)
end
end

defp run(pattern, value) do
case :re.compile(pattern) do
{:ok, compiled} ->
case :re.run(value, compiled) do
{:match, _captures} -> {:ok, true}
:nomatch -> {:ok, false}
end

{:error, _reason} ->
{:error, :invalid_pattern}
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Util.Mixfile do

def project do
[app: :util,
version: "0.0.1",
version: "0.1.0",
elixir: "~> 1.4",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
Expand Down
67 changes: 67 additions & 0 deletions test/safe_regex_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule Util.SafeRegexTest do
use ExUnit.Case, async: true

alias Util.SafeRegex

describe "validate_pattern/1" do
test "accepts a valid pattern" do
assert :ok = SafeRegex.validate_pattern("^[0-9]+$")
end

test "rejects nil" do
assert {:error, :invalid_pattern} = SafeRegex.validate_pattern(nil)
end

test "rejects empty pattern" do
assert {:error, :invalid_pattern} = SafeRegex.validate_pattern("")
end

test "rejects malformed pattern" do
assert {:error, :invalid_pattern} = SafeRegex.validate_pattern("[")
end

test "rejects pattern over the length cap" do
pattern = String.duplicate("a", SafeRegex.max_pattern_length() + 1)
assert {:error, :pattern_too_long} = SafeRegex.validate_pattern(pattern)
end
end

describe "match/2" do
test "returns {:ok, true} on match" do
assert {:ok, true} = SafeRegex.match("^[0-9]+$", "123")
end

test "returns {:ok, false} on no match" do
assert {:ok, false} = SafeRegex.match("^[0-9]+$", "abc")
end

test "rejects pattern over the length cap" do
pattern = String.duplicate("a", SafeRegex.max_pattern_length() + 1)
assert {:error, :pattern_too_long} = SafeRegex.match(pattern, "anything")
end

test "rejects value over the length cap" do
value = String.duplicate("a", SafeRegex.max_value_length() + 1)
assert {:error, :value_too_long} = SafeRegex.match("^a+$", value)
end

test "rejects malformed pattern" do
assert {:error, :invalid_pattern} = SafeRegex.match("[", "anything")
end

test "bounded execution of an adversarial pattern terminates" do
pattern = "^([a-zA-Z]+)*$"
value = String.duplicate("a", 50) <> "1"

assert {:ok, false} = SafeRegex.match(pattern, value)
end

test "treats nil value as no match" do
assert {:ok, false} = SafeRegex.match("^a+$", nil)
end

test "treats nil pattern as invalid" do
assert {:error, :invalid_pattern} = SafeRegex.match(nil, "anything")
end
end
end