diff --git a/lib/plug/static.ex b/lib/plug/static.ex index df6f7ba7..6320472c 100644 --- a/lib/plug/static.ex +++ b/lib/plug/static.ex @@ -204,7 +204,7 @@ defmodule Plug.Static do :forbidden -> conn - status -> + :allowed -> segments = Enum.map(segments, &URI.decode/1) if invalid_path?(segments) do @@ -215,17 +215,19 @@ defmodule Plug.Static do range = get_req_header(conn, "range") case file_encoding(conn, path, range, encodings) do - :error -> - conn + :error -> conn + triplet -> serve_static(triplet, conn, segments, range, options) + end - triplet -> - if status == :raise do - raise InvalidPathError, - "static file exists but is not in the :only list: #{Enum.join(segments, "/")}. " <> - "Add it to the :only list or use :only_matching for prefix matching" - end + :raise -> + segments = Enum.map(segments, &URI.decode/1) - serve_static(triplet, conn, segments, range, options) + if not invalid_path?(segments) and regular_file_info(path(from, segments)) do + raise InvalidPathError, + "static file exists but is not in the :only list: #{Enum.join(segments, "/")}. " <> + "Add it to the :only list or use :only_matching for prefix matching" + else + conn end end end diff --git a/test/plug/static_test.exs b/test/plug/static_test.exs index 3917ee6f..b116c8c9 100644 --- a/test/plug/static_test.exs +++ b/test/plug/static_test.exs @@ -895,4 +895,69 @@ defmodule Plug.StaticTest do assert conn.status == 200 end + + describe "raise_on_missing_only" do + test "passes through when path has colons and does not match :only" do + conn = + conn(:get, "/public/resource:identifier") + |> call(only: ["assets"], raise_on_missing_only: true) + + assert conn.status == 404 + assert conn.resp_body == "Passthrough" + end + + test "passes through when path has dot-dot segments and does not match :only" do + conn = + conn(:get, "/public/..%2Fsecret") + |> call(only: ["assets"], raise_on_missing_only: true) + + assert conn.status == 404 + assert conn.resp_body == "Passthrough" + end + + test "passes through when path has null bytes and does not match :only" do + conn = + conn(:get, "/public/file%00.txt") + |> call(only: ["assets"], raise_on_missing_only: true) + + assert conn.status == 404 + assert conn.resp_body == "Passthrough" + end + + test "passes through when non-existent file does not match :only" do + conn = + conn(:get, "/public/nonexistent.txt") + |> call(only: ["assets"], raise_on_missing_only: true) + + assert conn.status == 404 + assert conn.resp_body == "Passthrough" + end + + test "passes through with only_matching when path does not match prefix" do + conn = + conn(:get, "/public/resource:identifier") + |> call(only_matching: ["assets"], raise_on_missing_only: true) + + assert conn.status == 404 + assert conn.resp_body == "Passthrough" + end + + test "raises when existing file is not in :only list" do + assert_raise Plug.Static.InvalidPathError, + ~r/static file exists but is not in the :only list/, + fn -> + conn(:get, "/public/fixtures/static.txt") + |> call(only: ["assets"], raise_on_missing_only: true) + end + end + + test "serves file normally when it matches :only list" do + conn = + conn(:get, "/public/fixtures/static.txt") + |> call(only: ["fixtures"], raise_on_missing_only: true) + + assert conn.status == 200 + assert conn.resp_body == "HELLO" + end + end end