From 9be3b8023b87af495e1f907ae4f5d1957504a8f1 Mon Sep 17 00:00:00 2001 From: Jeff Hlywa Date: Wed, 3 Oct 2018 23:24:04 -0400 Subject: [PATCH] Backport safe_relative_path/1 from OTP 19.3 --- src/elli_static.erl | 42 +++++++++++++++++++++++++++++++++++++- test/elli_static_tests.erl | 20 ++++++++++++++---- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/elli_static.erl b/src/elli_static.erl index ee71698..db48384 100644 --- a/src/elli_static.erl +++ b/src/elli_static.erl @@ -88,7 +88,7 @@ maybe_file(Req, Prefix, Dir) -> %% santize the path ensuring the request doesn't access any parent %% directories ... and reattach the slash if deemed safe - SafePath = case filename:safe_relative_path(RawPath) of + SafePath = case safe_relative_path(RawPath) of unsafe -> throw(?NOT_FOUND); %% return type quirk work around @@ -110,3 +110,43 @@ maybe_file(Req, Prefix, Dir) -> _ -> nothing end. + + +%% OTP_RELEASE macro was introduced in 21, `filename:safe_relative_path/1' in +%% 19.3, so the code below is safe +-ifdef(OTP_RELEASE). + -ifdef(?OTP_RELEASE >= 21). +safe_relative_path(Path) -> + filename:safe_relative_path(Path). + -endif. +-else. + +%% @doc Backport of `filename:safe_relative_path/1' from 19.3. This code was +%% lifted from: +%% https://github.com/erlang/otp/blob/master/lib/stdlib/src/filename.erl#L811 +-spec safe_relative_path(binary()) -> unsafe | file:name_all(). +safe_relative_path(Path) -> + case filename:pathtype(Path) of + relative -> + Cs0 = filename:split(Path), + safe_relative_path_1(Cs0, []); + _ -> + unsafe + end. + +safe_relative_path_1([<<".">>|T], Acc) -> + safe_relative_path_1(T, Acc); +safe_relative_path_1([<<"..">>|T], Acc) -> + climb(T, Acc); +safe_relative_path_1([H|T], Acc) -> + safe_relative_path_1(T, [H|Acc]); +safe_relative_path_1([], []) -> + []; +safe_relative_path_1([], Acc) -> + filename:join(lists:reverse(Acc)). + +climb(_, []) -> + unsafe; +climb(T, [_|Acc]) -> + safe_relative_path_1(T, Acc). +-endif. diff --git a/test/elli_static_tests.erl b/test/elli_static_tests.erl index f6e8c49..56a889d 100644 --- a/test/elli_static_tests.erl +++ b/test/elli_static_tests.erl @@ -9,7 +9,8 @@ elli_static_test_() -> ?_test(no_file()), ?_test(not_found()), ?_test(safe_traversal()), - ?_test(unsafe_traversal())]}. + ?_test(unsafe_traversal()), + ?_test(invalid_path_separator())]}. readme() -> @@ -31,13 +32,18 @@ not_found() -> ?assertMatch({{"HTTP/1.1",404,"Not Found"}, _Headers, "Not Found"}, Response). safe_traversal() -> - {ok, Response} = httpc:request("http://localhost:3000/elli_static/" - "../elli_static/README.md"), {ok, File} = file:read_file("README.md"), Expected = binary_to_list(File), + + {ok, Response} = httpc:request("http://localhost:3000/elli_static/" + "../elli_static/README.md"), ?assertEqual([integer_to_list(iolist_size(Expected))], proplists:get_all_values("content-length", element(2, Response))), - ?assertMatch({_Status, _Headers, Expected}, Response). + ?assertMatch({_Status, _Headers, Expected}, Response), + + + %% `Response' should match the same request above + {ok, Response} = httpc:request("http://localhost:3000/elli_static/./README.md"). unsafe_traversal() -> %% compute the relative path to /etc/passwd @@ -48,6 +54,12 @@ unsafe_traversal() -> {ok, Response} = httpc:request("http://localhost:3000/elli_static/" ++ Path), ?assertMatch({{"HTTP/1.1",404,"Not Found"}, _Headers, "Not Found"}, Response). +invalid_path_separator() -> + %% https://www.ietf.org/rfc/rfc2396.txt defines a path separator to be a + %% single slash + {ok, Response} = httpc:request("http://localhost:3000////elli_static/README.md"), + ?assertMatch({{"HTTP/1.1",404,"Not Found"}, _Headers, "Not Found"}, Response). + setup() -> {ok, Dir} = file:get_cwd(), Args = [{<<"/elli_static">>, {dir, Dir}}],