Skip to content

Commit e5a91fb

Browse files
bitterpanda63claude
andcommitted
Count 404 requests with foreign extensions as attack wave scans
Ports AikidoSec/firewall-node#1041 to Python. Requests to foreign-platform extensions (php, java, jsp, etc.) are only counted as scan hits when the response status code is 404 — a 200 may indicate the Python app is proxying to another backend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3734f28 commit e5a91fb

7 files changed

Lines changed: 82 additions & 45 deletions

File tree

aikido_zen/sources/functions/request_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def post_response(status_code):
106106
if cache.is_bypassed_ip(context.remote_address):
107107
return
108108

109-
attack_wave = attack_wave_detector_store.is_attack_wave(context)
109+
attack_wave = attack_wave_detector_store.is_attack_wave(context, status_code)
110110
if attack_wave:
111111
cache.stats.on_detected_attack_wave(blocked=False)
112112

aikido_zen/storage/attack_wave_detector_store.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ def __init__(self):
1010
self._detector = AttackWaveDetector()
1111
self._lock = threading.RLock() # Reentrant lock for thread safety
1212

13-
def is_attack_wave(self, context: Context) -> bool:
13+
def is_attack_wave(self, context: Context, status_code: int = 404) -> bool:
1414
with self._lock:
15-
return self._detector.is_attack_wave(context)
15+
return self._detector.is_attack_wave(context, status_code)
1616

1717
def get_samples_for_ip(self, ip: str):
1818
with self._lock:

aikido_zen/vulnerabilities/attack_wave_detection/attack_wave_detector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(
3434
time_to_live_in_ms=self.attack_wave_time_frame,
3535
)
3636

37-
def is_attack_wave(self, context: Context) -> bool:
37+
def is_attack_wave(self, context: Context, status_code: int = 404) -> bool:
3838
"""
3939
Function gets called with context to check if there is an attack wave request.
4040
"""
@@ -45,7 +45,7 @@ def is_attack_wave(self, context: Context) -> bool:
4545
if self.sent_events_map.get(ip) is not None:
4646
return False
4747

48-
if not is_web_scanner(context):
48+
if not is_web_scanner(context, status_code):
4949
return False
5050

5151
# Increment suspicious requests count -> there is a new or first suspicious request

aikido_zen/vulnerabilities/attack_wave_detection/is_web_scan_path.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,31 @@
1616
"sqlitedb",
1717
"sqlite3db",
1818
}
19+
20+
# Extensions that belong to other platforms (e.g. PHP, Java).
21+
# A 200 response may mean the Python app is proxying to that backend,
22+
# so we only count these as scan hits when the response is 404.
23+
foreign_extensions = {
24+
"php",
25+
"php3",
26+
"php4",
27+
"php5",
28+
"phtml",
29+
"java",
30+
"jsp",
31+
"jspx",
32+
}
33+
1934
filenames = {name.lower() for name in file_names}
2035
directories = {name.lower() for name in directory_names}
2136

2237

23-
def is_web_scan_path(path: str) -> bool:
38+
def is_web_scan_path(path: str, status_code: int = 404) -> bool:
2439
"""
2540
is_web_scan_path gets the current route and wants to determine whether it's a test by some web scanner.
2641
Checks filename if it exists (list of suspicious filenames & list of supsicious extensions)
2742
Checks all other segments for suspicious directories
43+
Foreign-platform extensions (php, jsp, etc.) are only counted when status_code is 404.
2844
"""
2945
normalized = path.lower()
3046
segments = normalized.split("/")
@@ -40,6 +56,8 @@ def is_web_scan_path(path: str) -> bool:
4056
ext = filename.split(".")[-1]
4157
if ext in file_extensions:
4258
return True
59+
if ext in foreign_extensions and status_code == 404:
60+
return True
4361

4462
for directory in segments:
4563
if directory in directories:

aikido_zen/vulnerabilities/attack_wave_detection/is_web_scan_path_test.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,41 @@
88

99

1010
def test_is_web_scan_path():
11-
assert is_web_scan_path("/.env")
12-
assert is_web_scan_path("/test/.env")
13-
assert is_web_scan_path("/test/.env.bak")
14-
assert is_web_scan_path("/.git/config")
15-
assert is_web_scan_path("/.aws/config")
16-
assert is_web_scan_path("/some/path/.git/test")
17-
assert is_web_scan_path("/some/path/.gitlab-ci.yml")
18-
assert is_web_scan_path("/some/path/.github/workflows/test.yml")
19-
assert is_web_scan_path("/.travis.yml")
20-
assert is_web_scan_path("/../example/")
21-
assert is_web_scan_path("/./test")
22-
assert is_web_scan_path("/Cargo.lock")
23-
assert is_web_scan_path("/System32/test")
11+
assert is_web_scan_path("/.env", 404)
12+
assert is_web_scan_path("/test/.env", 404)
13+
assert is_web_scan_path("/test/.env.bak", 404)
14+
assert is_web_scan_path("/.git/config", 404)
15+
assert is_web_scan_path("/.aws/config", 404)
16+
assert is_web_scan_path("/some/path/.git/test", 404)
17+
assert is_web_scan_path("/some/path/.gitlab-ci.yml", 404)
18+
assert is_web_scan_path("/some/path/.github/workflows/test.yml", 404)
19+
assert is_web_scan_path("/.travis.yml", 404)
20+
assert is_web_scan_path("/../example/", 404)
21+
assert is_web_scan_path("/./test", 404)
22+
assert is_web_scan_path("/Cargo.lock", 404)
23+
assert is_web_scan_path("/System32/test", 404)
2424

2525

2626
def test_is_not_web_scan_path():
27-
assert not is_web_scan_path("/test/file.txt")
28-
assert not is_web_scan_path("/some/route/to/file.txt")
29-
assert not is_web_scan_path("/some/route/to/file.json")
30-
assert not is_web_scan_path("/en")
31-
assert not is_web_scan_path("/")
32-
assert not is_web_scan_path("/test/route")
33-
assert not is_web_scan_path("/static/file.css")
34-
assert not is_web_scan_path("/static/file.a461f56e.js")
27+
assert not is_web_scan_path("/test/file.txt", 404)
28+
assert not is_web_scan_path("/some/route/to/file.txt", 404)
29+
assert not is_web_scan_path("/some/route/to/file.json", 404)
30+
assert not is_web_scan_path("/en", 404)
31+
assert not is_web_scan_path("/", 404)
32+
assert not is_web_scan_path("/test/route", 404)
33+
assert not is_web_scan_path("/static/file.css", 404)
34+
assert not is_web_scan_path("/static/file.a461f56e.js", 404)
35+
36+
37+
def test_foreign_extensions_404():
38+
assert is_web_scan_path("/admin.php", 404)
39+
assert is_web_scan_path("/app.jsp", 404)
40+
41+
42+
def test_foreign_extensions_non_404():
43+
assert not is_web_scan_path("/admin.php", 200)
44+
assert not is_web_scan_path("/admin.php", 301)
45+
assert not is_web_scan_path("/app.jsp", 200)
3546

3647

3748
def test_no_duplicates_in_file_names():

aikido_zen/vulnerabilities/attack_wave_detection/is_web_scanner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
)
1111

1212

13-
def is_web_scanner(context: Context) -> bool:
13+
def is_web_scanner(context: Context, status_code: int = 404) -> bool:
1414
if context.method and is_web_scan_method(context.method):
1515
return True
16-
if context.route and is_web_scan_path(context.route):
16+
if context.route and is_web_scan_path(context.route, status_code):
1717
return True
1818
if query_params_contain_dangerous_strings(context):
1919
return True

aikido_zen/vulnerabilities/attack_wave_detection/is_web_scanner_test.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,30 @@ def get_test_context(path="/", method="GET", query=None):
2222

2323

2424
def test_is_web_scanner():
25-
assert is_web_scanner(get_test_context("/wp-config.php", "GET"))
26-
assert is_web_scanner(get_test_context("/.env", "GET"))
27-
assert is_web_scanner(get_test_context("/test/.env.bak", "GET"))
28-
assert is_web_scanner(get_test_context("/.git/config", "GET"))
29-
assert is_web_scanner(get_test_context("/.aws/config", "GET"))
30-
assert is_web_scanner(get_test_context("/../secret", "GET"))
31-
assert is_web_scanner(get_test_context("/", "BADMETHOD"))
32-
assert is_web_scanner(get_test_context("/", "GET", {"test": "SELECT * FROM admin"}))
33-
assert is_web_scanner(get_test_context("/", "GET", {"test": "../etc/passwd"}))
25+
assert is_web_scanner(get_test_context("/wp-config.php", "GET"), 404)
26+
assert is_web_scanner(get_test_context("/.env", "GET"), 404)
27+
assert is_web_scanner(get_test_context("/test/.env.bak", "GET"), 404)
28+
assert is_web_scanner(get_test_context("/.git/config", "GET"), 404)
29+
assert is_web_scanner(get_test_context("/.aws/config", "GET"), 404)
30+
assert is_web_scanner(get_test_context("/../secret", "GET"), 404)
31+
assert is_web_scanner(get_test_context("/", "BADMETHOD"), 404)
32+
assert is_web_scanner(get_test_context("/", "GET", {"test": "SELECT * FROM admin"}), 404)
33+
assert is_web_scanner(get_test_context("/", "GET", {"test": "../etc/passwd"}), 404)
3434

3535

3636
def test_is_not_web_scanner():
37-
assert not is_web_scanner(get_test_context("graphql", "POST"))
38-
assert not is_web_scanner(get_test_context("/api/v1/users", "GET"))
39-
assert not is_web_scanner(get_test_context("/public/index.html", "GET"))
40-
assert not is_web_scanner(get_test_context("/static/js/app.js", "GET"))
41-
assert not is_web_scanner(get_test_context("/uploads/image.png", "GET"))
42-
assert not is_web_scanner(get_test_context("/", "GET", {"test": "1'"}))
43-
assert not is_web_scanner(get_test_context("/", "GET", {"test": "abcd"}))
37+
assert not is_web_scanner(get_test_context("graphql", "POST"), 404)
38+
assert not is_web_scanner(get_test_context("/api/v1/users", "GET"), 404)
39+
assert not is_web_scanner(get_test_context("/public/index.html", "GET"), 404)
40+
assert not is_web_scanner(get_test_context("/static/js/app.js", "GET"), 404)
41+
assert not is_web_scanner(get_test_context("/uploads/image.png", "GET"), 404)
42+
assert not is_web_scanner(get_test_context("/", "GET", {"test": "1'"}), 404)
43+
assert not is_web_scanner(get_test_context("/", "GET", {"test": "abcd"}), 404)
44+
45+
46+
def test_foreign_extension_only_on_404():
47+
assert is_web_scanner(get_test_context("/admin.php", "GET"), 404)
48+
assert not is_web_scanner(get_test_context("/admin.php", "GET"), 200)
49+
assert not is_web_scanner(get_test_context("/admin.php", "GET"), 301)
50+
assert is_web_scanner(get_test_context("/app.jsp", "GET"), 404)
51+
assert not is_web_scanner(get_test_context("/app.jsp", "GET"), 200)

0 commit comments

Comments
 (0)