Skip to content

Commit 2495d69

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/zen-internals-ip-matcher
2 parents aa206e7 + 8f35368 commit 2495d69

9 files changed

Lines changed: 128 additions & 27 deletions

File tree

‎.github/workflows/smoke-test-ffi.yml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,32 @@ jobs:
5252

5353
- name: Run tests
5454
run: poetry run pytest aikido_zen/vulnerabilities/sql_injection/init_test.py
55+
56+
smoke-test-musl:
57+
runs-on: ${{ matrix.os }}
58+
strategy:
59+
fail-fast: false
60+
matrix:
61+
os: [ubuntu-24.04, ubuntu-22.04-arm]
62+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
63+
64+
steps:
65+
- name: Checkout code
66+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
67+
with:
68+
persist-credentials: false
69+
70+
- name: Download binaries
71+
run: make binaries && cp -r .cache/binaries/* aikido_zen/libs
72+
73+
# pip runs inside the container, so safe-chain is set up there and not on the runner.
74+
# build-base compiles pytricia, which has no prebuilt wheel.
75+
- name: Run tests on python:${{ matrix.python-version }}-alpine
76+
run: |
77+
docker run --rm -v "$(pwd):/app" -w /app python:${{ matrix.python-version }}-alpine sh -eo pipefail -c "
78+
apk add --no-cache build-base curl
79+
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
80+
export PATH=\"\$HOME/.safe-chain/shims:\$HOME/.safe-chain/bin:\$PATH\"
81+
pip install --root-user-action=ignore . pytest
82+
python -m pytest aikido_zen/helpers/zen_internals_test.py
83+
"

‎aikido_zen/helpers/match_endpoints.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,23 @@
22
Helper function file, see function docstring
33
"""
44

5+
from functools import lru_cache
6+
57
import regex as re
8+
from aikido_zen.helpers.logging import logger
69
from .try_parse_url_path import try_parse_url_path
710

811

12+
@lru_cache
13+
def compile_wildcard_route(route):
14+
try:
15+
return re.compile(f"^{route.replace('*', '(.*)')}/?$", re.IGNORECASE)
16+
except re.error as e:
17+
# Raising here would turn off protection for every request.
18+
logger.warning("Route regex is not compiling: %s (%s)", route, e)
19+
return None
20+
21+
922
def match_endpoints(route_metadata, endpoints):
1023
"""
1124
Based on the context's url this tries to find a match in the list of endpoints
@@ -44,10 +57,8 @@ def match_endpoints(route_metadata, endpoints):
4457
)
4558

4659
for wildcard in wildcards:
47-
route = wildcard["route"]
48-
regex = re.compile(f"^{route.replace('*', '(.*)')}/?$", re.IGNORECASE)
49-
50-
if regex.match(path):
60+
regex = compile_wildcard_route(wildcard["route"])
61+
if regex and regex.match(path):
5162
results.append(wildcard)
5263
if len(results) > 0:
5364
return results

‎aikido_zen/helpers/match_endpoints_test.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,50 @@ def test_it_prefers_specific_method_over_wildcard_second_case():
345345
}
346346

347347
assert result[0] == expected
348+
349+
350+
@pytest.mark.parametrize("route", ["/api/[/*", "/api/(/*", "/api/v1+?+/*"])
351+
def test_invalid_wildcard_route_is_skipped(route):
352+
endpoints = [
353+
{"method": "POST", "route": route, "forceProtectionOff": True},
354+
{"method": "POST", "route": "/api/*", "forceProtectionOff": False},
355+
]
356+
assert match_endpoints(
357+
sample_route_metadata(route=None, url="/api/[/x"), endpoints
358+
) == [endpoints[1]]
359+
360+
361+
def test_wildcard_route_keeps_regex_characters_that_compile():
362+
endpoints = [{"method": "POST", "route": "/v1.0/*", "forceProtectionOff": False}]
363+
for url in ["/v1.0/abc", "/v1x0/abc"]:
364+
assert (
365+
match_endpoints(sample_route_metadata(route=None, url=url), endpoints)
366+
== endpoints
367+
)
368+
369+
370+
def test_wildcard_route_is_compiled_once(monkeypatch):
371+
endpoints = [
372+
{"method": "POST", "route": "/compiled-once/*", "forceProtectionOff": False}
373+
]
374+
route_metadata = sample_route_metadata(route=None, url="/compiled-once/x")
375+
assert match_endpoints(route_metadata, endpoints) == endpoints
376+
377+
def fail_to_compile(*args):
378+
raise AssertionError("compiled the route again")
379+
380+
monkeypatch.setattr(
381+
"aikido_zen.helpers.match_endpoints.re.compile", fail_to_compile
382+
)
383+
for _ in range(3):
384+
assert match_endpoints(route_metadata, endpoints) == endpoints
385+
386+
387+
def test_invalid_wildcard_route_is_logged_once(caplog):
388+
endpoints = [{"method": "POST", "route": "/logged-once/[/*"}]
389+
route_metadata = sample_route_metadata(route=None, url="/logged-once/x")
390+
for _ in range(3):
391+
assert match_endpoints(route_metadata, endpoints) is None
392+
warnings = [r for r in caplog.records if "/logged-once/[/*" in r.getMessage()]
393+
assert len(warnings) == 1
394+
assert warnings[0].levelname == "WARNING"

‎aikido_zen/vulnerabilities/init_test.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ def test_sql_injection_but_blocking_off(caplog, get_context, monkeypatch):
118118
assert get_cache().stats.get_record()["requests"]["attacksDetected"]["blocked"] == 0
119119

120120

121+
def test_sql_injection_with_invalid_wildcard_route(caplog, get_context, monkeypatch):
122+
get_context.set_as_current_context()
123+
monkeypatch.setenv("AIKIDO_BLOCK", "1")
124+
get_cache().config.set_endpoints(
125+
[{"method": "GET", "route": "/api/[/*", "forceProtectionOff": False}]
126+
)
127+
with pytest.raises(AikidoSQLInjection):
128+
run_vulnerability_scan(
129+
kind="sql_injection",
130+
op="test_op",
131+
args=("INSERT * INTO VALUES ('doggoss2', TRUE);", "mysql"),
132+
)
133+
134+
121135
def test_sql_injection_with_route_params(caplog, get_context, monkeypatch):
122136
get_context.set_as_current_context()
123137
monkeypatch.setenv("AIKIDO_BLOCK", "1")

‎poetry.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎sample-apps/flask-mongo/poetry.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎sample-apps/flask-mssql/poetry.lock‎

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎sample-apps/flask-mysql/poetry.lock‎

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎sample-apps/flask-openai/poetry.lock‎

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)