Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/aikido/zen/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ def extract_payloads_from(data, source_type, prefix = nil, depth = 0)
end

def unsafe_path?(filepath)
# Check for terminal ".." components before normalization
# to catch cases like File.join(base, "..", "file") where cleanpath
# would normalize away the traversal
downcase_filepath = filepath.to_s.downcase
return true if downcase_filepath == ".."
return true if downcase_filepath.end_with?("/..")
return true if downcase_filepath.end_with?("\\..")

normalized_filepath = Pathname.new(filepath).cleanpath.to_s.downcase

Scanners::PathTraversal::DANGEROUS_PATH_PARTS.each do |dangerous_path_part|
Expand Down
8 changes: 8 additions & 0 deletions lib/aikido/zen/scanners/path_traversal/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ def self.include_unsafe_path_parts?(filepath)
return true if filepath.include?(dangerous_part)
end

# Check for terminal ".." components that could be used in path traversal
# when combined with other path components (e.g., File.join(base, "..", "file"))
# This catches cases where the input is exactly ".." or ends with "/.." or "\.."
# without a trailing separator, which would bypass the DANGEROUS_PATH_PARTS check
return true if filepath == ".."
return true if filepath.end_with?("/..")
return true if filepath.end_with?("\\..")

false
end

Expand Down
13 changes: 13 additions & 0 deletions test/aikido/zen/context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,19 @@ def stub_payload(source, value, path)
assert_includes context.payloads, stub_payload(:body, "/etc/passwd", "path3.__File.join__")
end

test "terminal .. component in arrays is detected for File.join" do
# Test the bypass scenario where an array contains exactly ".."
# which would be composed with other path components
context = build_context_for("/example", {
:method => "POST",
:input => %({"path1": ["uploads", "..", "secret"], "path2": ["..", "secret"]}),
"CONTENT_TYPE" => "application/json"
})

assert_includes context.payloads, stub_payload(:body, "uploads/../secret", "path1.__File.join__")
assert_includes context.payloads, stub_payload(:body, "../secret", "path2.__File.join__")
end

test "route payloads are read from the data extracted by the router" do
router = MockedRailsRouter.build do
match "/example/:resource(/:id)(.:format)",
Expand Down
14 changes: 13 additions & 1 deletion test/aikido/zen/scanners/path_traversal_scanner_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def scan(filepath, input = query)
refute_attack "directory/file.txt", "directory/file.txt"
end

test "it flags bad inputs" do
test \"it flags bad inputs\" do
# inputs with ../
assert_attack "../file.txt", "../"
assert_attack "../file.txt", "../file.txt"
Expand All @@ -65,6 +65,18 @@ def scan(filepath, input = query)
assert_attack "./../../file.txt", "./../../file.txt"
end

test "it flags terminal .. components used in path composition" do
# Terminal ".." without trailing separator (e.g., from File.join(base, "..", "file"))
assert_attack "/srv/app/uploads/../secret", ".."
assert_attack "/base/path/../file.txt", ".."
assert_attack "c:\\base\\path\\..\\file.txt", ".."

# Terminal ".." at end of path segments
assert_attack "/srv/app/uploads/..", "uploads/.."
assert_attack "/srv/app/uploads/..", "app/uploads/.."
assert_attack "c:\\base\\path\\..", "path\\.."
end

test "linux paths" do
refute_attack "/etc/passwd", "/etc/"
assert_attack "/etc/passwd", "/etc/passwd"
Expand Down
14 changes: 14 additions & 0 deletions test/aikido/zen/sinks/file_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,20 @@ def assert_path_traversal_attack(operation, &block)
File.realdirpath OFFENDER_PATH
end
end

test "terminal .. component bypass is detected" do
# Test the specific bypass scenario where input is exactly ".."
# and gets composed with File.join(base, "..", "file")
set_context_from_request_to "/?dir=.."

error = assert_attack Aikido::Zen::Attacks::PathTraversalAttack do
# Simulating: File.join("/srv/app/uploads", params[:dir], "secret")
path = File.join("/srv/app/uploads", "..", "secret")
File.read(path) rescue nil
end

assert_equal error.attack.operation, "File.read"
end
end

module Helpers
Expand Down
Loading