From fa1b1baa49efeb65133f3fd8e72d01710574984b 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:42:26 +0000 Subject: [PATCH] fix(security): Fixed path traversal bypass by comparing normalized paths correctly --- .../zen/scanners/path_traversal/helpers.rb | 10 ++--- .../scanners/path_traversal_scanner_test.rb | 45 ++++++++++++------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/lib/aikido/zen/scanners/path_traversal/helpers.rb b/lib/aikido/zen/scanners/path_traversal/helpers.rb index 1982a50e..58a2e17a 100644 --- a/lib/aikido/zen/scanners/path_traversal/helpers.rb +++ b/lib/aikido/zen/scanners/path_traversal/helpers.rb @@ -67,11 +67,11 @@ def self.start_with_unsafe_path?(filepath, user_input) DANGEROUS_PATH_STARTS.each do |dangerous_start| if normalized_path.start_with?(dangerous_start) && normalized_path.start_with?(normalized_user_input) - # If the user input is the same as the dangerous start, we don't want to flag it - # to prevent false positives. - # e.g., if user input is /etc/ and the path is /etc/passwd, we don't want to flag it, - # as long as the user input does not contain a subdirectory or filename - return false if user_input == dangerous_start || user_input == dangerous_start.chomp("/") + # If the normalized filepath equals the normalized user input, we don't want to flag it + # to prevent false positives when accessing exactly the dangerous directory itself. + # e.g., if user input is /etc and the path is /etc, we don't want to flag it. + # However, if the path is /etc/passwd and user input is /etc, this should be flagged. + return false if normalized_path == normalized_user_input return true end diff --git a/test/aikido/zen/scanners/path_traversal_scanner_test.rb b/test/aikido/zen/scanners/path_traversal_scanner_test.rb index 1a49f0b2..50415abd 100644 --- a/test/aikido/zen/scanners/path_traversal_scanner_test.rb +++ b/test/aikido/zen/scanners/path_traversal_scanner_test.rb @@ -66,39 +66,39 @@ def scan(filepath, input = query) end test "linux paths" do - refute_attack "/etc/passwd", "/etc/" + assert_attack "/etc/passwd", "/etc/" assert_attack "/etc/passwd", "/etc/passwd" assert_attack "/etc/../etc/passwd", "/etc/../etc/passwd" assert_attack "/home/user/file.txt", "/home/user" end test "common container/cloud directories" do - refute_attack "/app/file.txt", "/app/" + assert_attack "/app/file.txt", "/app/" assert_attack "/app/file.txt", "/app/file.txt" assert_attack "/app/../app/file.txt", "/app/../app/file.txt" assert_attack "/app/user/file.txt", "/app/user" - refute_attack "/code/file.txt", "/code/" + assert_attack "/code/file.txt", "/code/" assert_attack "/code/file.txt", "/code/file.txt" assert_attack "/code/../code/file.txt", "/code/../code/file.txt" assert_attack "/code/user/file.txt", "/code/user" - refute_attack "/data/file.txt", "/data/" + assert_attack "/data/file.txt", "/data/" assert_attack "/data/file.txt", "/data/file.txt" assert_attack "/data/../data/file.txt", "/data/../data/file.txt" assert_attack "/data/user/file.txt", "/data/user" - refute_attack "/rails/file.txt", "/rails/" + assert_attack "/rails/file.txt", "/rails/" assert_attack "/rails/file.txt", "/rails/file.txt" assert_attack "/rails/../rails/file.txt", "/rails/../rails/file.txt" assert_attack "/rails/app/file.txt", "/rails/app" - refute_attack "/workspace/file.txt", "/workspace/" + assert_attack "/workspace/file.txt", "/workspace/" assert_attack "/workspace/file.txt", "/workspace/file.txt" assert_attack "/workspace/../workspace/file.txt", "/workspace/../workspace/file.txt" assert_attack "/workspace/project/file.txt", "/workspace/project" - refute_attack "/workspaces/file.txt", "/workspaces/" + assert_attack "/workspaces/file.txt", "/workspaces/" assert_attack "/workspaces/file.txt", "/workspaces/file.txt" assert_attack "/workspaces/../workspaces/file.txt", "/workspaces/../workspaces/file.txt" assert_attack "/workspaces/project/file.txt", "/workspaces/project" @@ -159,13 +159,20 @@ def scan(filepath, input = query) refute_attack "./~root/file.txt/some-file", "~root/file.txt" end - test "does not detect if user input path contains no filename or subfolder" do - refute_attack "/etc/app/test.txt", "/etc/" - refute_attack "/etc/app/", "/etc/" - refute_attack "/etc/app/", "/etc" + test "does not detect if user input path equals the filepath" do refute_attack "/etc/", "/etc/" refute_attack "/etc", "/etc" - refute_attack "/var/a", "/var/" + refute_attack "/var/a", "/var/a" + end + + test "detects attack if user input is a dangerous directory and filepath is a descendant" do + assert_attack "/etc/app/test.txt", "/etc/" + assert_attack "/etc/app/", "/etc/" + assert_attack "/etc/app/", "/etc" + assert_attack "/var/a", "/var/" + end + + test "does not detect if user input is not a prefix of filepath" do refute_attack "/var/a", "/var/b" refute_attack "/var/a", "/var/b/test.txt" end @@ -185,10 +192,16 @@ def scan(filepath, input = query) assert_attack "///.///etc/passwd", "///.///etc/passwd" end - test "normalized paths still trigger false positive prevention for bare root dirs" do - # User input that resolves to just a root dir should still be safe - refute_attack "/etc/./passwd", "/etc" - refute_attack "//etc//passwd", "/etc" + test "normalized paths still trigger false positive prevention for exact matches only" do + # User input that resolves to exactly the same path as filepath should still be safe + refute_attack "/etc", "/etc" + refute_attack "//etc//", "/etc" + end + + test "normalized paths detect attacks when filepath is a descendant" do + # User input that resolves to a parent directory should be flagged when filepath is a descendant + assert_attack "/etc/./passwd", "/etc" + assert_attack "//etc//passwd", "/etc" end test "it does dected if user input path contains a filename or subfolder" do