diff --git a/README.md b/README.md index 3207c01..c799b01 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/ex_sieve/builder/where.ex b/lib/ex_sieve/builder/where.ex index 6a901c9..ceb9eaa 100644 --- a/lib/ex_sieve/builder/where.ex +++ b/lib/ex_sieve/builder/where.ex @@ -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 @@ -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]) @@ -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 diff --git a/test/ex_sieve/builder/where_test.exs b/test/ex_sieve/builder/where_test.exs index a40faaf..83459d3 100644 --- a/test/ex_sieve/builder/where_test.exs +++ b/test/ex_sieve/builder/where_test.exs @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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)