Skip to content

Commit

Permalink
Backport safe_relative_path/1 from OTP 19.3
Browse files Browse the repository at this point in the history
  • Loading branch information
jhlywa committed Oct 8, 2018
1 parent 89ad172 commit 9be3b80
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 5 deletions.
42 changes: 41 additions & 1 deletion src/elli_static.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
20 changes: 16 additions & 4 deletions test/elli_static_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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() ->
Expand All @@ -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
Expand All @@ -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}}],
Expand Down

0 comments on commit 9be3b80

Please sign in to comment.