Skip to content

Commit bc8b579

Browse files
committed
Skip wave-attack detection for bypassed requests
Adds a defensive bypass-flag check inside is_attack_wave so that wave detection consistently honours the BypassedContextStore signal even if a caller forgot to skip post_response for a bypassed request. Also adds two regression tests: - 16 requests with distinct query strings produce 15 unique samples (previously collapsed to 1 sample because URL stripped the query string). - Bypassed flag set: is_attack_wave returns False and never collects samples. Re-enables test_wave_attack in the QA suite. The QA test relies on: - The BypassedContextStore wiring from "Skip context creation for bypassed IPs". - The query-string preservation from "Include query string in URL for ASGI/WSGI requests".
1 parent 30bc365 commit bc8b579

5 files changed

Lines changed: 122 additions & 1 deletion

File tree

.github/workflows/qa-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ jobs:
5252
app_port: 8080
5353
sleep_before_test: 30
5454
config_update_delay: 100
55-
skip_tests: test_bypassed_ip_for_geo_blocking,test_demo_apps_generic_tests,test_outbound_domain_blocking,test_bypassed_ip,test_wave_attack,test_block_traffic_by_countries,test_user_rate_limiting_1_minute
55+
skip_tests: test_bypassed_ip_for_geo_blocking,test_demo_apps_generic_tests,test_outbound_domain_blocking,test_bypassed_ip,test_block_traffic_by_countries,test_user_rate_limiting_1_minute

aikido_zen/storage/attack_wave_detector_store_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,17 @@
1010
AttackWaveDetectorStore,
1111
attack_wave_detector_store,
1212
)
13+
from . import bypassed_context_store
1314
import aikido_zen.test_utils as test_utils
1415

1516

17+
@pytest.fixture(autouse=True)
18+
def _clear_bypass_flag():
19+
bypassed_context_store.clear()
20+
yield
21+
bypassed_context_store.clear()
22+
23+
1624
def test_attack_wave_detector_store_initialization():
1725
"""Test that the store initializes correctly"""
1826
store = AttackWaveDetectorStore()
@@ -436,6 +444,49 @@ def create_context_with_url(ip, url, method="GET"):
436444
assert set(sample.keys()) == {"method", "url"}
437445

438446

447+
def test_samples_collect_one_per_unique_url():
448+
"""16 requests with distinct query strings should produce 16 unique samples."""
449+
store = AttackWaveDetectorStore()
450+
ip = "2.16.53.8"
451+
452+
with patch(
453+
"aikido_zen.vulnerabilities.attack_wave_detection.attack_wave_detector.is_web_scanner",
454+
return_value=True,
455+
):
456+
for i in range(16):
457+
ctx = test_utils.generate_context(
458+
ip=ip,
459+
method="GET",
460+
url=f"http://localhost:3018/api/pets/?path=q{i}",
461+
)
462+
store.is_attack_wave(ctx)
463+
464+
samples = store.get_samples_for_ip(ip)
465+
assert len(samples) == 15, f"expected 15 samples, got {len(samples)}"
466+
467+
468+
def test_bypassed_request_skips_wave_detection():
469+
"""When the bypass flag is set, is_attack_wave never reports a wave."""
470+
store = AttackWaveDetectorStore()
471+
ip = "2.16.53.10"
472+
473+
with patch(
474+
"aikido_zen.vulnerabilities.attack_wave_detection.attack_wave_detector.is_web_scanner",
475+
return_value=True,
476+
):
477+
bypassed_context_store.set_bypassed(True)
478+
for i in range(16):
479+
ctx = test_utils.generate_context(
480+
ip=ip,
481+
method="GET",
482+
url=f"http://localhost:3018/api/execute/.env{i}",
483+
)
484+
assert store.is_attack_wave(ctx) is False
485+
486+
# Bypassed requests should not collect samples either.
487+
assert store.get_samples_for_ip(ip) == []
488+
489+
439490
@patch("aikido_zen.storage.attack_wave_detector_store.AttackWaveDetector")
440491
def test_mock_detector_integration(mock_detector_class):
441492
"""Test integration with mocked AttackWaveDetector"""
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
Records whether the current request's remote IP is in the bypass list.
3+
4+
Bypassed requests intentionally do not set a Context (so all per-request
5+
protection short-circuits), but checks that run without a Context — for
6+
example outbound DNS reporting — still need a way to detect "this work
7+
was triggered by a bypassed request".
8+
"""
9+
10+
import contextvars
11+
12+
_bypassed = contextvars.ContextVar("aikido_bypassed_ip", default=False)
13+
14+
15+
def set_bypassed(value: bool) -> None:
16+
_bypassed.set(bool(value))
17+
18+
19+
def is_bypassed() -> bool:
20+
try:
21+
return _bypassed.get()
22+
except Exception:
23+
return False
24+
25+
26+
def clear() -> None:
27+
_bypassed.set(False)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pytest
2+
3+
from aikido_zen.storage import bypassed_context_store
4+
5+
6+
@pytest.fixture(autouse=True)
7+
def _reset_after_each_test():
8+
yield
9+
bypassed_context_store.clear()
10+
11+
12+
def test_default_is_false():
13+
assert bypassed_context_store.is_bypassed() is False
14+
15+
16+
def test_set_bypassed_true_then_false():
17+
bypassed_context_store.set_bypassed(True)
18+
assert bypassed_context_store.is_bypassed() is True
19+
20+
bypassed_context_store.set_bypassed(False)
21+
assert bypassed_context_store.is_bypassed() is False
22+
23+
24+
def test_clear_resets_to_false():
25+
bypassed_context_store.set_bypassed(True)
26+
assert bypassed_context_store.is_bypassed() is True
27+
28+
bypassed_context_store.clear()
29+
assert bypassed_context_store.is_bypassed() is False
30+
31+
32+
def test_truthy_values_coerced_to_bool():
33+
bypassed_context_store.set_bypassed("yes")
34+
assert bypassed_context_store.is_bypassed() is True
35+
36+
bypassed_context_store.set_bypassed(0)
37+
assert bypassed_context_store.is_bypassed() is False

aikido_zen/vulnerabilities/attack_wave_detection/attack_wave_detector.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import aikido_zen.helpers.get_current_unixtime_ms as internal_time
22
from aikido_zen.ratelimiting.lru_cache import LRUCache
33
from aikido_zen.context import Context
4+
from aikido_zen.storage import bypassed_context_store
45
from aikido_zen.vulnerabilities.attack_wave_detection.is_web_scanner import (
56
is_web_scanner,
67
)
@@ -40,6 +41,11 @@ def is_attack_wave(self, context: Context) -> bool:
4041
"""
4142
if not context or not context.remote_address:
4243
return False
44+
if bypassed_context_store.is_bypassed():
45+
# Defensive: callers should normally never reach here for bypassed
46+
# IPs (the framework entry point clears the context), but keep wave
47+
# detection consistent with all other per-request blocking sites.
48+
return False
4349
ip = context.remote_address
4450

4551
if self.sent_events_map.get(ip) is not None:

0 commit comments

Comments
 (0)