From 20d31043aac9e2303d9e6ad1e3ac05f6fea03d0f Mon Sep 17 00:00:00 2001 From: "aikido-autofix[bot]" <119856028+aikido-autofix[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:43:05 +0000 Subject: [PATCH] fix(security): Fixed path-traversal bypass by detecting terminal ".." components before... --- lib/aikido/zen/context.rb | 8 ++++++++ lib/aikido/zen/scanners/path_traversal/helpers.rb | 8 ++++++++ test/aikido/zen/context_test.rb | 13 +++++++++++++ .../zen/scanners/path_traversal_scanner_test.rb | 14 +++++++++++++- test/aikido/zen/sinks/file_test.rb | 14 ++++++++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/aikido/zen/context.rb b/lib/aikido/zen/context.rb index 6a8ead23..68523343 100644 --- a/lib/aikido/zen/context.rb +++ b/lib/aikido/zen/context.rb @@ -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| diff --git a/lib/aikido/zen/scanners/path_traversal/helpers.rb b/lib/aikido/zen/scanners/path_traversal/helpers.rb index 1982a50e..c052ccd8 100644 --- a/lib/aikido/zen/scanners/path_traversal/helpers.rb +++ b/lib/aikido/zen/scanners/path_traversal/helpers.rb @@ -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 diff --git a/test/aikido/zen/context_test.rb b/test/aikido/zen/context_test.rb index d95a857d..3ffb1f21 100644 --- a/test/aikido/zen/context_test.rb +++ b/test/aikido/zen/context_test.rb @@ -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)", diff --git a/test/aikido/zen/scanners/path_traversal_scanner_test.rb b/test/aikido/zen/scanners/path_traversal_scanner_test.rb index 1a49f0b2..a90a85cb 100644 --- a/test/aikido/zen/scanners/path_traversal_scanner_test.rb +++ b/test/aikido/zen/scanners/path_traversal_scanner_test.rb @@ -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" @@ -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" diff --git a/test/aikido/zen/sinks/file_test.rb b/test/aikido/zen/sinks/file_test.rb index e65b66aa..d669711a 100644 --- a/test/aikido/zen/sinks/file_test.rb +++ b/test/aikido/zen/sinks/file_test.rb @@ -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