Skip to content

Commit b8d5430

Browse files
committed
Merge branch 'main' into add-aws-lambda-support-beta
2 parents 1f224dc + acf1166 commit b8d5430

File tree

8 files changed

+149
-17
lines changed

8 files changed

+149
-17
lines changed

aikido_zen/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from aikido_zen.lambda_helper import protect_lambda
1111
from aikido_zen.context.users import set_user
1212
from aikido_zen.helpers.check_gevent import check_gevent
13+
from aikido_zen.helpers.python_version_not_supported import python_version_not_supported
1314
from aikido_zen.middleware import should_block_request
1415
from aikido_zen.middleware.set_rate_limit_group import set_rate_limit_group
1516

@@ -35,6 +36,8 @@ def protect(mode="daemon", token=""):
3536
if aikido_disabled_flag_active():
3637
# Do not run any aikido code when the disabled flag is on
3738
return
39+
if python_version_not_supported():
40+
return
3841
if not test_uds_file_access():
3942
return # Unable to start background process
4043
if check_gevent():
@@ -72,6 +75,7 @@ def protect(mode="daemon", token=""):
7275

7376
import aikido_zen.sinks.builtins
7477
import aikido_zen.sinks.os
78+
import aikido_zen.sinks.pathlib
7579
import aikido_zen.sinks.shutil
7680
import aikido_zen.sinks.io
7781
import aikido_zen.sinks.http_client
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import sys
2+
from aikido_zen.helpers.logging import logger
3+
4+
5+
def python_version_not_supported() -> bool:
6+
major = sys.version_info.major
7+
minor = sys.version_info.minor
8+
if major != 3:
9+
logger.error("This version of Zen only supports Python 3")
10+
return True
11+
if minor > 13:
12+
logger.error("This version of Zen doesn't support versions above Python 3.13")
13+
return True
14+
return False

aikido_zen/sinks/builtins_import.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ def _import(func, instance, args, kwargs, return_value):
1414
return
1515
running_import_scan.set(True)
1616

17-
if not hasattr(return_value, "__file__"):
18-
return # Would be built-in into the interpreter (system package)
17+
if not hasattr(return_value, "__package__"):
18+
return
1919

20-
if not hasattr(return_value, "__package__"):
20+
try:
21+
if running_import_scan.get():
2122
return
23+
running_import_scan.set(True)
24+
2225
name = getattr(return_value, "__package__")
2326

2427
if not name:

aikido_zen/sinks/pathlib.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Sink module for python's `pathlib`
3+
"""
4+
5+
import aikido_zen.vulnerabilities as vulns
6+
from aikido_zen.helpers.get_argument import get_argument
7+
from aikido_zen.helpers.register_call import register_call
8+
from aikido_zen.sinks import before, patch_function, on_import
9+
10+
11+
@before
12+
def _pathlib_truediv_patch(func, instance, args, kwargs):
13+
path = get_argument(args, kwargs, 0, "key")
14+
op = "pathlib.PurePath.__truediv__"
15+
register_call(op, "fs_op")
16+
17+
vulns.run_vulnerability_scan(kind="path_traversal", op=op, args=(path,))
18+
19+
20+
@on_import("pathlib")
21+
def patch(m):
22+
"""
23+
patching module pathlib
24+
- patches PurePath.__truediv__ : Path() / Path() -> join operation
25+
"""
26+
27+
# PurePath() / "my/path/test.txt"
28+
# This is accomplished by overloading the __truediv__ function on the Path class
29+
patch_function(m, "PurePath.__truediv__", _pathlib_truediv_patch)

aikido_zen/sinks/tests/builtins_import_test.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,35 @@ def test_django_import():
2222
import django
2323

2424
assert PackagesStore.get_package("django")["version"] == "4.0"
25+
26+
27+
def test_recursive_package_store(monkeypatch):
28+
"""Test that recursive imports during package scanning don't cause max recursion depth errors."""
29+
30+
def recursive_get_package(name):
31+
"""Recursively add package and its dependencies to PackagesStore."""
32+
import flask
33+
34+
PackagesStore.clear()
35+
monkeypatch.setattr(PackagesStore, "get_package", recursive_get_package)
36+
37+
import flask
38+
39+
# Restore the original method after the test
40+
monkeypatch.undo()
41+
42+
43+
def test_recursive_package_store_2(monkeypatch):
44+
"""Test that recursive imports during package scanning don't cause max recursion depth errors."""
45+
46+
def recursive_add_package(name, version):
47+
"""Recursively add package and its dependencies to PackagesStore."""
48+
if name == "django":
49+
import django
50+
51+
PackagesStore.clear()
52+
monkeypatch.setattr(PackagesStore, "add_package", recursive_add_package)
53+
import django
54+
55+
# Restore the original method after the test
56+
monkeypatch.undo()

aikido_zen/sinks/tests/os_test.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import pytest
22
from pathlib import Path, PurePath
33
from unittest.mock import patch
4-
import aikido_zen.sinks.os
4+
import aikido_zen
5+
import aikido_zen.test_utils as test_utils
6+
7+
aikido_zen.protect()
8+
from aikido_zen.context import Context
9+
from aikido_zen.errors import AikidoPathTraversal
510

611
kind = "path_traversal"
712

813

14+
@pytest.fixture(autouse=True)
15+
def set_blocking_to_true(monkeypatch):
16+
monkeypatch.setenv("AIKIDO_BLOCK", "1")
17+
18+
919
def test_ospath_commands():
1020
with patch(
1121
"aikido_zen.vulnerabilities.run_vulnerability_scan"
@@ -39,6 +49,36 @@ def test_ospath_commands():
3949
mock_run_vulnerability_scan.assert_any_call(kind=kind, op=op, args=args)
4050

4151

52+
def test_os_create_path_with_multiple_slashes():
53+
import os
54+
55+
file_path = "////etc/passwd"
56+
test_utils.generate_and_set_context(file_path)
57+
with pytest.raises(AikidoPathTraversal):
58+
full_path = Path("flaskr/resources/blogs/") / file_path
59+
open(full_path, "r").close()
60+
61+
62+
def test_os_create_path_with_multiple_double_slashes():
63+
import os
64+
65+
file_path = "////etc//passwd"
66+
test_utils.generate_and_set_context(file_path)
67+
with pytest.raises(AikidoPathTraversal):
68+
full_path = Path("flaskr/resources/blogs/") / file_path
69+
open(full_path, "r").close()
70+
71+
72+
def test_os_path_traversal_with_multiple_slashes():
73+
import os
74+
75+
file_path = "home///..////..////my_secret.txt"
76+
test_utils.generate_and_set_context(file_path)
77+
with pytest.raises(AikidoPathTraversal):
78+
full_path = Path("flaskr/resources/blogs/") / file_path
79+
open(full_path, "r").close()
80+
81+
4282
def test_ospath_command_absolute_path():
4383
with patch(
4484
"aikido_zen.vulnerabilities.run_vulnerability_scan"

aikido_zen/vulnerabilities/path_traversal/unsafe_path_start.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,19 @@
2828

2929
def starts_with_unsafe_path(file_path, user_input):
3030
"""Check if the file path starts with any dangerous paths and the user input."""
31-
lower_case_path = file_path.lower()
32-
lower_case_user_input = user_input.lower()
31+
path_parsed = ensure_one_leading_slash(file_path.lower())
32+
input_parsed = ensure_one_leading_slash(user_input.lower())
3333

3434
for dangerous_start in dangerous_path_starts:
35-
if lower_case_path.startswith(dangerous_start) and lower_case_path.startswith(
36-
lower_case_user_input
35+
if path_parsed.startswith(dangerous_start) and path_parsed.startswith(
36+
input_parsed
3737
):
3838
return True
3939

4040
return False
41+
42+
43+
def ensure_one_leading_slash(path: str) -> str:
44+
if path.startswith("/"):
45+
return "/" + path.lstrip("/")
46+
return path

end2end/django_mysql_test.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def test_initial_heartbeat():
8282
assert len(heartbeat_events) == 1
8383
validate_heartbeat(
8484
heartbeat_events[0],
85-
[{
85+
routes=[{
8686
"apispec": {
8787
'body': {
8888
'type': 'form-urlencoded',
@@ -103,12 +103,16 @@ def test_initial_heartbeat():
103103
"hits_delta_since_sync": 0,
104104
"method": "POST",
105105
"path": "/app/create"
106-
}],
107-
{
108-
"aborted": 0,
109-
"attacksDetected": {"blocked": 2, "total": 2},
110-
"total": 3,
111-
'rateLimited': 0
112-
},
113-
{'wrapt', 'asgiref', 'aikido_zen', 'django', 'sqlparse', 'mysqlclient'}
106+
}],
107+
packages={'wrapt', 'asgiref', 'aikido_zen', 'django', 'sqlparse', 'mysqlclient'}
114108
)
109+
req_stats = heartbeat_events[0]["stats"]["requests"]
110+
assert req_stats["aborted"] == 0
111+
assert req_stats["rateLimited"] == 0
112+
assert req_stats["attacksDetected"] == {"blocked": 2, "total": 2}
113+
# There are 3-4 requests :
114+
# 1. is website live request, first request not always counted
115+
# 2. /app/create safe
116+
# 3. /app/create sql inj
117+
# 4. /app/shell/ls -la shell inj
118+
assert 3 <= req_stats["total"] <= 4, f"Unexpected amount of total requests {req_stats['total']}"

0 commit comments

Comments
 (0)