summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrea Leopardi <an.leopardi@gmail.com>2022-01-05 18:52:15 +0200
committerGitHub <noreply@github.com>2022-01-05 18:52:15 +0200
commit64ac646a24f8e0d7f0eb6f0ae2152a578df5cebe (patch)
treeb9afa1e35d10a38b3cd599a61c9deb3caa8fcf9f
parent5792c3835fdef6fbf52fc477ef9789f96c0ec68d (diff)
downloadelixir-64ac646a24f8e0d7f0eb6f0ae2152a578df5cebe.tar.gz
Add Path.safe_relative/1 and Path.safe_relative_to/2 (#11542)
-rw-r--r--lib/elixir/lib/path.ex70
-rw-r--r--lib/elixir/test/elixir/path_test.exs31
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"