Skip to content

Commit

Permalink
[Add] properly escape like queries
Browse files Browse the repository at this point in the history
  • Loading branch information
albertored committed Jun 25, 2020
1 parent 550bcac commit eebb1b4
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 36 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@ and

You can read more about predicates on [ransack wiki page](https://github.com/activerecord-hackery/ransack/wiki/Basic-Searching).

#### Note

`LIKE` queries can suffer of [LIKE injection](https://github.blog/2015-11-03-like-injection/) attacks.

For this reason all predicates which result in a `LIKE` query (`cont`, `not_cont`, `start`, `not_start`, `end`, `not_end`
and their composite predicates) are properly escaped.

Some exceptions are `matches`, `does_not_match` and their composite predicates that allows `%`, `_` and `\` chars in the value.
You should be very careful when allowing an external user to use these predicates.

## Contributing

First, you'll need to build the test database.
Expand Down
26 changes: 14 additions & 12 deletions lib/ex_sieve/builder/where.ex
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,19 @@ defmodule ExSieve.Builder.Where do
end

defp build_dynamic(:cont, %Attribute{parent: [], name: name}, [value | _]) do
dynamic([p], ilike(field(p, ^name), ^"%#{value}%"))
dynamic([p], ilike(field(p, ^name), ^"%#{escape_like_value(value)}%"))
end

defp build_dynamic(:cont, %Attribute{parent: parent, name: name}, [value | _]) do
dynamic([{^parent_name(parent), p}], ilike(field(p, ^name), ^"%#{value}%"))
dynamic([{^parent_name(parent), p}], ilike(field(p, ^name), ^"%#{escape_like_value(value)}%"))
end

defp build_dynamic(:not_cont, %Attribute{parent: [], name: name}, [value | _]) do
dynamic([p], not ilike(field(p, ^name), ^"%#{value}%"))
dynamic([p], not ilike(field(p, ^name), ^"%#{escape_like_value(value)}%"))
end

defp build_dynamic(:not_cont, %Attribute{parent: parent, name: name}, [value | _]) do
dynamic([{^parent_name(parent), p}], not ilike(field(p, ^name), ^"%#{value}%"))
dynamic([{^parent_name(parent), p}], not ilike(field(p, ^name), ^"%#{escape_like_value(value)}%"))
end

defp build_dynamic(:lt, %Attribute{parent: [], name: name}, [value | _]) do
Expand Down Expand Up @@ -228,35 +228,35 @@ defmodule ExSieve.Builder.Where do
end

defp build_dynamic(:start, %Attribute{parent: [], name: name}, [value | _]) do
dynamic([p], ilike(field(p, ^name), ^"#{value}%"))
dynamic([p], ilike(field(p, ^name), ^"#{escape_like_value(value)}%"))
end

defp build_dynamic(:start, %Attribute{parent: parent, name: name}, [value | _]) do
dynamic([{^parent_name(parent), p}], ilike(field(p, ^name), ^"#{value}%"))
dynamic([{^parent_name(parent), p}], ilike(field(p, ^name), ^"#{escape_like_value(value)}%"))
end

defp build_dynamic(:not_start, %Attribute{parent: [], name: name}, [value | _]) do
dynamic([p], not ilike(field(p, ^name), ^"#{value}%"))
dynamic([p], not ilike(field(p, ^name), ^"#{escape_like_value(value)}%"))
end

defp build_dynamic(:not_start, %Attribute{parent: parent, name: name}, [value | _]) do
dynamic([{^parent_name(parent), p}], not ilike(field(p, ^name), ^"#{value}%"))
dynamic([{^parent_name(parent), p}], not ilike(field(p, ^name), ^"#{escape_like_value(value)}%"))
end

defp build_dynamic(:end, %Attribute{parent: [], name: name}, [value | _]) do
dynamic([p], ilike(field(p, ^name), ^"%#{value}"))
dynamic([p], ilike(field(p, ^name), ^"%#{escape_like_value(value)}"))
end

defp build_dynamic(:end, %Attribute{parent: parent, name: name}, [value | _]) do
dynamic([{^parent_name(parent), p}], ilike(field(p, ^name), ^"%#{value}"))
dynamic([{^parent_name(parent), p}], ilike(field(p, ^name), ^"%#{escape_like_value(value)}"))
end

defp build_dynamic(:not_end, %Attribute{parent: [], name: name}, [value | _]) do
dynamic([p], not ilike(field(p, ^name), ^"%#{value}"))
dynamic([p], not ilike(field(p, ^name), ^"%#{escape_like_value(value)}"))
end

defp build_dynamic(:not_end, %Attribute{parent: parent, name: name}, [value | _]) do
dynamic([{^parent_name(parent), p}], not ilike(field(p, ^name), ^"%#{value}"))
dynamic([{^parent_name(parent), p}], not ilike(field(p, ^name), ^"%#{escape_like_value(value)}"))
end

defp build_dynamic(true, attribute, _value), do: build_dynamic(:eq, attribute, [true])
Expand Down Expand Up @@ -300,4 +300,6 @@ defmodule ExSieve.Builder.Where do
end

defp build_dynamic(_predicate, _attribute, _values), do: {:error, :predicate_not_found}

defp escape_like_value(value), do: Regex.replace(~r/([\%_])/, value, ~S(\\\1))
end
48 changes: 24 additions & 24 deletions test/ex_sieve/builder/where_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,12 @@ defmodule ExSieve.Builder.WhereTest do
end

test ":cont" do
{base, ex_sieve} = ex_sieve_post_query(%{"body_cont" => "foo"}, false)
query = base |> where([p], ilike(field(p, :body), ^"%foo%")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"body_cont" => "f%o_o"}, false)
query = base |> where([p], ilike(field(p, :body), ^"%f\\%o\\_o%")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"body_cont" => "foo"}, true)
query = base |> where([posts: p], ilike(field(p, :body), ^"%foo%")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"body_cont" => "f%o_o"}, true)
query = base |> where([posts: p], ilike(field(p, :body), ^"%f\\%o\\_o%")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"id_cont" => 1}, false)
Expand All @@ -161,12 +161,12 @@ defmodule ExSieve.Builder.WhereTest do
end

test ":not_cont" do
{base, ex_sieve} = ex_sieve_post_query(%{"body_not_cont" => "foo"}, false)
query = base |> where([p], not ilike(field(p, :body), ^"%foo%")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"body_not_cont" => "f%o_o"}, false)
query = base |> where([p], not ilike(field(p, :body), ^"%f\\%o\\_o%")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"body_not_cont" => "foo"}, true)
query = base |> where([posts: p], not ilike(field(p, :body), ^"%foo%")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"body_not_cont" => "f%o_o"}, true)
query = base |> where([posts: p], not ilike(field(p, :body), ^"%f\\%o\\_o%")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"id_not_cont" => 1}, false)
Expand Down Expand Up @@ -269,12 +269,12 @@ defmodule ExSieve.Builder.WhereTest do
end

test ":start" do
{base, ex_sieve} = ex_sieve_post_query(%{"title_start" => "foo"}, false)
query = base |> where([p], ilike(field(p, :title), ^"foo%")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"title_start" => "f%o_o"}, false)
query = base |> where([p], ilike(field(p, :title), ^"f\\%o\\_o%")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"title_start" => "foo"}, true)
query = base |> where([posts: p], ilike(field(p, :title), ^"foo%")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"title_start" => "f%o_o"}, true)
query = base |> where([posts: p], ilike(field(p, :title), ^"f\\%o\\_o%")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"published_start" => true}, false)
Expand All @@ -285,12 +285,12 @@ defmodule ExSieve.Builder.WhereTest do
end

test ":not_start" do
{base, ex_sieve} = ex_sieve_post_query(%{"title_not_start" => "foo"}, false)
query = base |> where([p], not ilike(field(p, :title), ^"foo%")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"title_not_start" => "f%o_o"}, false)
query = base |> where([p], not ilike(field(p, :title), ^"f\\%o\\_o%")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"title_not_start" => "foo"}, true)
query = base |> where([posts: p], not ilike(field(p, :title), ^"foo%")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"title_not_start" => "f%o_o"}, true)
query = base |> where([posts: p], not ilike(field(p, :title), ^"f\\%o\\_o%")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"published_not_start" => true}, false)
Expand All @@ -301,12 +301,12 @@ defmodule ExSieve.Builder.WhereTest do
end

test ":end" do
{base, ex_sieve} = ex_sieve_post_query(%{"title_end" => "foo"}, false)
query = base |> where([p], ilike(field(p, :title), ^"%foo")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"title_end" => "f%o_o"}, false)
query = base |> where([p], ilike(field(p, :title), ^"%f\\%o\\_o")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"title_end" => "foo"}, true)
query = base |> where([posts: p], ilike(field(p, :title), ^"%foo")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"title_end" => "f%o_o"}, true)
query = base |> where([posts: p], ilike(field(p, :title), ^"%f\\%o\\_o")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"published_end" => true}, false)
Expand All @@ -317,12 +317,12 @@ defmodule ExSieve.Builder.WhereTest do
end

test ":not_end" do
{base, ex_sieve} = ex_sieve_post_query(%{"title_not_end" => "foo"}, false)
query = base |> where([p], not ilike(field(p, :title), ^"%foo")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"title_not_end" => "f%o_o"}, false)
query = base |> where([p], not ilike(field(p, :title), ^"%f\\%o\\_o")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"title_not_end" => "foo"}, true)
query = base |> where([posts: p], not ilike(field(p, :title), ^"%foo")) |> inspect()
{base, ex_sieve} = ex_sieve_post_query(%{"title_not_end" => "f%o_o"}, true)
query = base |> where([posts: p], not ilike(field(p, :title), ^"%f\\%o\\_o")) |> inspect()
assert query == ex_sieve

{base, ex_sieve} = ex_sieve_post_query(%{"published_not_end" => true}, false)
Expand Down

0 comments on commit eebb1b4

Please sign in to comment.