Skip to content

Commit 88a7df1

Browse files
Merge branch 'main' into ffi-arm
2 parents 8d2cabc + 40a2926 commit 88a7df1

File tree

15 files changed

+812
-49
lines changed

15 files changed

+812
-49
lines changed
Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import http from 'k6/http';
2-
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';
3-
import { check, sleep, fail } from 'k6';
4-
import exec from 'k6/execution';
5-
import { Trend } from 'k6/metrics';
2+
import {Trend} from 'k6/metrics';
63

74
const HTTP = {
85
withZen: {
@@ -16,59 +13,58 @@ const HTTP = {
1613
}
1714

1815
function test(name, fn) {
19-
const duration = tests[name].duration;
20-
const overhead = tests[name].overhead;
21-
2216
const withZen = fn(HTTP.withZen);
2317
const withoutZen = fn(HTTP.withoutZen);
18+
const timeWithZen = withZen.timings.duration;
19+
const timeWithoutZen = withoutZen.timings.duration;
2420

25-
const timeWithZen = withZen.timings.duration,
26-
timeWithoutZen = withoutZen.timings.duration;
27-
28-
duration.add(timeWithZen - timeWithoutZen);
21+
tests[name].delta.add(timeWithZen - timeWithoutZen);
22+
tests[name].overhead.add(100 * (timeWithZen - timeWithoutZen) / timeWithoutZen)
2923

30-
const ratio = withZen.timings.duration / withoutZen.timings.duration;
31-
overhead.add(100 * (timeWithZen - timeWithoutZen) / timeWithoutZen)
24+
tests[name].with_zen.add(timeWithZen);
25+
tests[name].without_zen.add(timeWithoutZen);
3226
}
3327

34-
const defaultHeaders = {
35-
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
36-
};
28+
function buildTestTrends(prefix) {
29+
return {
30+
delta: new Trend(`${prefix}_delta`),
31+
with_zen: new Trend(`${prefix}_with_zen`),
32+
without_zen: new Trend(`${prefix}_without_zen`),
33+
overhead: new Trend(`${prefix}_overhead`)
34+
};
35+
}
3736

3837
const tests = {
39-
test_post_page_with_json_body: {
40-
duration: new Trend("test_post_page_with_json_body"),
41-
overhead: new Trend("test_overhead_with_json_body")
42-
},
43-
test_get_page_without_attack: {
44-
duration: new Trend("test_get_page_without_attack"),
45-
overhead: new Trend("test_overhead_without_attack")
46-
},
47-
test_get_page_with_sql_injection: {
48-
duration: new Trend("test_get_page_with_sql_injection"),
49-
overhead: new Trend("test_overhead_with_sql_injection"),
50-
}
38+
test_post_page_with_json_body: buildTestTrends("test_post_page_with_json_body"),
39+
test_get_page_without_attack: buildTestTrends("test_get_page_without_attack"),
40+
test_get_page_with_sql_injection: buildTestTrends("test_get_page_with_sql_injection")
5141
}
5242
export const options = {
5343
vus: 1, // Number of virtual users
5444
iterations: 200,
5545
thresholds: {
56-
test_post_page_with_json_body: ["med<10"],
57-
test_get_page_without_attack: ["med<10"],
58-
test_get_page_with_sql_injection: ["med<10"],
46+
http_req_failed: ['rate==0'], // we are marking the attacks as expected, so we should have no errors
47+
test_post_page_with_json_body_delta: ["med<10"],
48+
test_get_page_without_attack_delta: ["med<10"],
49+
test_get_page_with_sql_injection_delta: ["med<10"],
5950
}
6051
};
6152

62-
const expectAttack = http.expectedStatuses(500);
53+
const expectAttack = http.expectedStatuses(200, 500);
6354

6455
export default function () {
6556
test("test_post_page_with_json_body",
6657
(http) => http.post("/cats", JSON.stringify({cat: {name: "Féline Dion"}}), {
67-
headers: {"Content-Type": "application/json"}
58+
headers: {
59+
"Content-Type": "application/json",
60+
"Accept": "application/json"
61+
}
6862
})
6963
)
64+
7065
test("test_get_page_without_attack", (http) => http.get("/cats"))
66+
7167
test("test_get_page_with_sql_injection", (http) =>
72-
http.get("/cats/1'%20OR%20''='", {responseCallback: expectAttack})
68+
http.get("/cats/1'%20OR%20''='", { responseCallback: expectAttack })
7369
)
7470
}

lib/aikido/zen/attack.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,39 @@ def exception(*)
3939
end
4040

4141
module Attacks
42+
class PathTraversalAttack < Attack
43+
attr_reader :input
44+
attr_reader :filepath
45+
46+
def initialize(input:, filepath:, **opts)
47+
super(**opts)
48+
@input = input
49+
@filepath = filepath
50+
end
51+
52+
def log_message
53+
format(
54+
"Path Traversal: Malicious user input «%s» detected while calling method %s",
55+
@input, @operation
56+
)
57+
end
58+
59+
def as_json
60+
{
61+
kind: "path_traversal",
62+
blocked: blocked?,
63+
metadata: {
64+
expanded_filepath: filepath
65+
},
66+
operation: @operation
67+
}.merge(@input.as_json)
68+
end
69+
70+
def exception(*)
71+
PathTraversalError.new(self)
72+
end
73+
end
74+
4275
class SQLInjectionAttack < Attack
4376
attr_reader :query
4477
attr_reader :input

lib/aikido/zen/errors.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ class SSRFDetectedError < UnderAttackError
7575
def_delegators :@attack, :request, :input
7676
end
7777

78+
class PathTraversalError < UnderAttackError
79+
extend Forwardable
80+
def_delegators :@attack, :input
81+
end
82+
7883
# Raised when there's any problem communicating (or loading) libzen.
7984
class InternalsError < ZenError
8085
# @param attempt [String] description of what we were trying to do.

lib/aikido/zen/scanners.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
require_relative "scanners/sql_injection_scanner"
44
require_relative "scanners/stored_ssrf_scanner"
55
require_relative "scanners/ssrf_scanner"
6+
require_relative "scanners/path_traversal_scanner"
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
3+
module Aikido::Zen
4+
module Scanners
5+
module PathTraversal
6+
DANGEROUS_PATH_PARTS = ["../", "..\\"]
7+
LINUX_ROOT_FOLDERS = [
8+
"/bin/",
9+
"/boot/",
10+
"/dev/",
11+
"/etc/",
12+
"/home/",
13+
"/init/",
14+
"/lib/",
15+
"/media/",
16+
"/mnt/",
17+
"/opt/",
18+
"/proc/",
19+
"/root/",
20+
"/run/",
21+
"/sbin/",
22+
"/srv/",
23+
"/sys/",
24+
"/tmp/",
25+
"/usr/",
26+
"/var/"
27+
]
28+
29+
DANGEROUS_PATH_STARTS = LINUX_ROOT_FOLDERS + ["c:/", "c:\\"]
30+
31+
module Helpers
32+
def self.contains_unsafe_path_parts(filepath)
33+
DANGEROUS_PATH_PARTS.each do |dangerous_part|
34+
return true if filepath.include?(dangerous_part)
35+
end
36+
37+
false
38+
end
39+
40+
def self.starts_with_unsafe_path(filepath, user_input)
41+
# Check if path is relative (not absolute or drive letter path)
42+
# Required because `expand_path` will build absolute paths from relative paths
43+
return false if Pathname.new(filepath).relative? || Pathname.new(user_input).relative?
44+
45+
normalized_path = File.expand_path__internal_for_aikido_zen(filepath).downcase
46+
normalized_user_input = File.expand_path__internal_for_aikido_zen(user_input).downcase
47+
48+
DANGEROUS_PATH_STARTS.each do |dangerous_start|
49+
if normalized_path.start_with?(dangerous_start) && normalized_path.start_with?(normalized_user_input)
50+
# If the user input is the same as the dangerous start, we don't want to flag it
51+
# to prevent false positives.
52+
# e.g., if user input is /etc/ and the path is /etc/passwd, we don't want to flag it,
53+
# as long as the user input does not contain a subdirectory or filename
54+
if user_input == dangerous_start || user_input == dangerous_start.chomp("/")
55+
return false
56+
end
57+
return true
58+
end
59+
end
60+
false
61+
end
62+
end
63+
end
64+
end
65+
end
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "path_traversal/helpers"
4+
5+
module Aikido::Zen
6+
module Scanners
7+
class PathTraversalScanner
8+
# Checks if the user introduced input is trying to access other path using
9+
# Path Traversal kind of attacks.
10+
#
11+
# @param filepath [String] the expanded path that is tried to be read
12+
# @param context [Aikido::Zen::Context]
13+
# @param sink [Aikido::Zen::Sink] the Sink that is running the scan.
14+
# @param operation [Symbol, String] name of the method being scanned.
15+
#
16+
# @return [Aikido::Zen::Attacks::PathTraversalAttack, nil] an Attack if any
17+
# user input is detected to be attempting a Path Traversal Attack, or +nil+ if not.
18+
def self.call(filepath:, sink:, context:, operation:)
19+
return unless context
20+
21+
context.payloads.each do |payload|
22+
next unless new(filepath, payload.value).attack?
23+
24+
return Attacks::PathTraversalAttack.new(
25+
sink: sink,
26+
input: payload,
27+
filepath: filepath,
28+
context: context,
29+
operation: "#{sink.operation}.#{operation}"
30+
)
31+
end
32+
33+
nil
34+
end
35+
36+
def initialize(filepath, input)
37+
@filepath = filepath.downcase
38+
@input = input.downcase
39+
end
40+
41+
def attack?
42+
# Single character are ignored because they don't pose a big threat
43+
return false if @input.length <= 1
44+
45+
# We ignore cases where the user input is longer than the file path.
46+
# Because the user input can't be part of the file path.
47+
return false if @input.length > @filepath.length
48+
49+
# We ignore cases where the user input is not part of the file path.
50+
return false unless @filepath.include?(@input)
51+
52+
if PathTraversal::Helpers.contains_unsafe_path_parts(@filepath) && PathTraversal::Helpers.contains_unsafe_path_parts(@input)
53+
return true
54+
end
55+
56+
# Check for absolute path traversal
57+
PathTraversal::Helpers.starts_with_unsafe_path(@filepath, @input)
58+
end
59+
end
60+
end
61+
end

lib/aikido/zen/sinks.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require_relative "sinks/socket"
66

77
require_relative "sinks/action_controller" if defined?(::ActionController)
8+
require_relative "sinks/file" if defined?(::File)
89
require_relative "sinks/resolv" if defined?(::Resolv)
910
require_relative "sinks/net_http" if defined?(::Net::HTTP)
1011
require_relative "sinks/http" if defined?(::HTTP)

0 commit comments

Comments
 (0)