diff options
author | Andrea Leopardi <an.leopardi@gmail.com> | 2022-01-05 18:52:15 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-05 18:52:15 +0200 |
commit | 64ac646a24f8e0d7f0eb6f0ae2152a578df5cebe (patch) | |
tree | b9afa1e35d10a38b3cd599a61c9deb3caa8fcf9f | |
parent | 5792c3835fdef6fbf52fc477ef9789f96c0ec68d (diff) | |
download | elixir-64ac646a24f8e0d7f0eb6f0ae2152a578df5cebe.tar.gz |
Add Path.safe_relative/1 and Path.safe_relative_to/2 (#11542)
-rw-r--r-- | lib/elixir/lib/path.ex | 70 | ||||
-rw-r--r-- | lib/elixir/test/elixir/path_test.exs | 31 |
2 files changed, 101 insertions, 0 deletions
diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 8b73a74eb..52bad2d2b 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -721,4 +721,74 @@ defmodule Path do defp major_os_type do :os.type() |> elem(0) end + + @doc """ + Returns a relative path that is protected from directory-traversal attacks. + + The given relative path is sanitized by eliminating `..` and `.` components. + + This function checks that, after expanding those components, the path is still "safe". + Paths are considered unsafe if either of these is true: + + * The path is not relative, such as `"/foo/bar"`. + + * A `..` component would make it so that the path would travers up above + the root of `relative_to`. + + * A symbolic link in the path points to something above the root of `relative_to`. + + ## Examples + + iex> Path.safe_relative_to("deps/my_dep/app.beam", "deps") + {:ok, "deps/my_dep/app.beam"} + + iex> Path.safe_relative_to("deps/my_dep/./build/../app.beam", "deps") + {:ok, "deps/my_dep/app.beam"} + + iex> Path.safe_relative_to("my_dep/../..", "deps") + :error + + iex> Path.safe_relative_to("/usr/local", ".") + :error + + """ + @doc since: "1.14.0" + @spec safe_relative_to(t, t) :: {:ok, binary} | :error + def safe_relative_to(path, relative_to) do + path = IO.chardata_to_string(path) + + case :filelib.safe_relative_path(path, relative_to) do + :unsafe -> :error + relative_path -> {:ok, IO.chardata_to_string(relative_path)} + end + end + + @doc """ + Returns a path relative to the current working directory that is + protected from directory-traversal attacks. + + Same as `safe_relative_to/2` with the current working directory as + the second argument. If there is an issue retrieving the current working + directory, this function raises an error. + + ## Examples + + iex> Path.safe_relative("foo") + {:ok, "foo"} + + iex> Path.safe_relative("foo/../bar") + {:ok, "bar"} + + iex> Path.safe_relative("foo/../..") + :error + + iex> Path.safe_relative("/usr/local") + :error + + """ + @doc since: "1.14.0" + @spec safe_relative(t) :: {:ok, binary} | :error + def safe_relative(path) do + safe_relative_to(path, File.cwd!()) + end end diff --git a/lib/elixir/test/elixir/path_test.exs b/lib/elixir/test/elixir/path_test.exs index 44aa57707..0a3702e49 100644 --- a/lib/elixir/test/elixir/path_test.exs +++ b/lib/elixir/test/elixir/path_test.exs @@ -101,6 +101,21 @@ defmodule PathTest do assert Path.split("C:\\foo\\bar") == ["c:/", "foo", "bar"] assert Path.split("C:/foo/bar") == ["c:/", "foo", "bar"] end + + test "safe_relative/1" do + assert Path.safe_relative("local/foo") == {:ok, "local/foo"} + assert Path.safe_relative("D:/usr/local/foo") == :error + assert Path.safe_relative("d:/usr/local/foo") == :error + assert Path.safe_relative("foo/../..") == :error + end + + test "safe_relative_to/2" do + assert Path.safe_relative_to("local/foo/bar", "local") == {:ok, "foo/bar"} + assert Path.safe_relative_to("foo/..", "local") == {:ok, "local"} + assert Path.safe_relative_to("..", "local/foo") == :error + assert Path.safe_relative_to("d:/usr/local/foo", "D:/") == :error + assert Path.safe_relative_to("D:/usr/local/foo", "d:/") == :error + end end describe "Unix" do @@ -215,6 +230,22 @@ defmodule PathTest do assert Path.relative_to(["usr", ?/, 'local/foo'], 'usr/local') == "foo" end + test "safe_relative/1" do + assert Path.safe_relative("foo/bar") == {:ok, "foo/bar"} + assert Path.safe_relative("foo/..") == {:ok, ""} + assert Path.safe_relative("./foo") == {:ok, "foo"} + + assert Path.safe_relative("/usr/local/foo") == :error + assert Path.safe_relative("foo/../..") == :error + end + + test "safe_relative_to/2" do + assert Path.safe_relative_to("/usr/local/foo", "/usr/local") == :error + assert Path.safe_relative_to("../../..", "foo/bar") == :error + assert Path.safe_relative_to("../../..", "foo/bar") == :error + assert Path.safe_relative_to("/usr/local/foo", "/") == :error + end + test "rootname/2" do assert Path.rootname("~/foo/bar.ex", ".ex") == "~/foo/bar" assert Path.rootname("~/foo/bar.exs", ".ex") == "~/foo/bar.exs" |