Skip to content

Commit a5725c0

Browse files
Masamuneeeioquatix
authored andcommitted
Prevent directory traversal via root prefix bypass.
1 parent 175e7d2 commit a5725c0

File tree

3 files changed

+32
-1
lines changed

3 files changed

+32
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. For info on
77
### Security
88

99
- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
10+
- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
1011

1112
## [2.2.21] - 2025-11-03
1213

lib/rack/directory.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def DIR_FILE_escape(htmls)
7676
# Set the root directory and application for serving files.
7777
def initialize(root, app = nil)
7878
@root = ::File.expand_path(root)
79+
@root_with_separator = @root.end_with?(::File::SEPARATOR) ? @root : "#{@root}#{::File::SEPARATOR}"
7980
@app = app || Files.new(@root)
8081
@head = Head.new(method(:get))
8182
end
@@ -112,7 +113,9 @@ def check_bad_request(path_info)
112113
# Rack response to use for requests with paths outside the root, or nil if path is inside the root.
113114
def check_forbidden(path_info)
114115
return unless path_info.include? ".."
115-
return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
116+
117+
expanded_path = ::File.expand_path(::File.join(@root, path_info))
118+
return if expanded_path == @root || expanded_path.start_with?(@root_with_separator)
116119

117120
body = "Forbidden\n"
118121
[403, { CONTENT_TYPE => "text/plain",

test/spec_directory.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,33 @@ def setup
128128
res.must_be :forbidden?
129129
end
130130

131+
it "not allow directory traversal via root prefix bypass" do
132+
Dir.mktmpdir do |dir|
133+
root = File.join(dir, "root")
134+
outside = "#{root}_test"
135+
FileUtils.mkdir_p(root)
136+
FileUtils.mkdir_p(outside)
137+
FileUtils.touch(File.join(outside, "test.txt"))
138+
139+
app = Rack::Directory.new(root)
140+
res = Rack::MockRequest.new(app).get("/../#{File.basename(outside)}/")
141+
142+
res.must_be :forbidden?
143+
end
144+
end
145+
146+
it "not allow dir globs" do
147+
Dir.mktmpdir do |dir|
148+
weirds = "uploads/.?/.?"
149+
full_dir = File.join(dir, weirds)
150+
FileUtils.mkdir_p full_dir
151+
FileUtils.touch File.join(dir, "secret.txt")
152+
app = Rack::Directory.new(File.join(dir, "uploads"))
153+
res = Rack::MockRequest.new(app).get("/.%3F")
154+
refute_match "secret.txt", res.body
155+
end
156+
end
157+
131158
it "404 if it can't find the file" do
132159
res = Rack::MockRequest.new(Rack::Lint.new(app)).
133160
get("/cgi/blubb")

0 commit comments

Comments
 (0)