From a5725c031b2717758851f1eadd9b9dfe7555745a Mon Sep 17 00:00:00 2001
From: Masamune <125840508+Masamuneee@users.noreply.github.com>
Date: Mon, 16 Feb 2026 10:21:43 +0700
Subject: [PATCH] Prevent directory traversal via root prefix bypass.

Backport note: v2.1.4's check_forbidden already forbids all paths
containing "..". The @root_with_separator instance variable is added
for defence-in-depth. The relaxed expanded_path check from the upstream
v2.2.22 fix is intentionally omitted because it would allow ".." paths
that resolve within root, breaking the existing strict behaviour.

---
 lib/rack/directory.rb  | 1 +
 test/spec_directory.rb | 15 +++++++++++++++
 2 files changed, 16 insertions(+)

--- a/lib/rack/directory.rb
+++ b/lib/rack/directory.rb
@@ -61,6 +61,7 @@
 
     def initialize(root, app = nil)
       @root = ::File.expand_path(root)
+      @root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
       @app = app || Rack::Files.new(@root)
       @head = Rack::Head.new(lambda { |env| get env })
     end
--- a/test/spec_directory.rb
+++ b/test/spec_directory.rb
@@ -105,6 +105,21 @@
     end
   end
 
+  it "not allow directory traversal via root prefix bypass" do
+    Dir.mktmpdir do |dir|
+      root = File.join(dir, "root")
+      outside = "#{root}_test"
+      FileUtils.mkdir_p(root)
+      FileUtils.mkdir_p(outside)
+      FileUtils.touch(File.join(outside, "test.txt"))
+
+      app = Rack::Directory.new(root)
+      res = Rack::MockRequest.new(app).get("/../#{File.basename(outside)}/")
+
+      res.must_be :forbidden?
+    end
+  end
+
   it "404 if it can't find the file" do
     res = Rack::MockRequest.new(Rack::Lint.new(app)).
       get("/cgi/blubb")
