Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
db6151b
Create new attack-wave_detection vuln and port is_web_scan_path/method
bitterpanda63 Sep 10, 2025
34b0cd5
Add query_params_contain_dangerous_strings
bitterpanda63 Sep 10, 2025
ac9f7a8
Port is_web_scanner.py
bitterpanda63 Sep 10, 2025
9bf6c8d
use cache string extraction for attack wave detection in query params
bitterpanda63 Sep 10, 2025
4381b01
Add test cases for is_web_scan_path
bitterpanda63 Sep 10, 2025
d742f45
add test cases to is_web_scan_method
bitterpanda63 Sep 10, 2025
9553549
lint
bitterpanda63 Sep 10, 2025
4fbe510
Add test cases for query_params_contain_dangerous_strings
bitterpanda63 Sep 10, 2025
c5a9474
is_web_scanner test cases
bitterpanda63 Sep 10, 2025
31d6049
Add benchmarks for is_web_scanner
bitterpanda63 Sep 10, 2025
04c72c4
create attack_wave_detector
bitterpanda63 Sep 10, 2025
ec8bee6
add test cases for the attack wave detector
bitterpanda63 Sep 10, 2025
90063ec
Add attack wave detector and on detected attack wave to cloud connect…
bitterpanda63 Sep 10, 2025
5a82406
Create an on_detected_attack_wave function that reports attack waves …
bitterpanda63 Sep 10, 2025
0d2af82
Allow for multiple attack types in the queue
bitterpanda63 Sep 10, 2025
66d59a5
check_firewall_lists, use a class as a common data type, and check at…
bitterpanda63 Sep 10, 2025
683e11e
Also do attack wave detection in the request handler
bitterpanda63 Sep 10, 2025
04a0dc3
attack_wave_detector check() function, assume that attack wave happened
bitterpanda63 Sep 10, 2025
171d15a
Add attack waves to statistics
bitterpanda63 Sep 10, 2025
7660300
request handler, report event via comms & update stats
bitterpanda63 Sep 10, 2025
49f2b63
Rename to check_firewall_lists_res
bitterpanda63 Sep 10, 2025
e50c085
Fix circular logging import in on_detected_attack_wave
bitterpanda63 Sep 10, 2025
5347e99
Update attack waves
bitterpanda63 Sep 10, 2025
54dfea0
update sync_data test cases
bitterpanda63 Sep 10, 2025
6697505
Add test cases for stats
bitterpanda63 Sep 10, 2025
47b76a1
Fix request_handler and check_firewall_lists
bitterpanda63 Sep 10, 2025
fc46f2f
Add test cases for attack waves
bitterpanda63 Sep 10, 2025
6a5dc03
Fix test cases in thread_cahce by adding attackWaves
bitterpanda63 Sep 10, 2025
b3231dc
context, reset cache before save
bitterpanda63 Sep 10, 2025
a801eb5
django_mysql e2e test case, add empty attackWaves
bitterpanda63 Sep 10, 2025
8654e40
remove useless comment
bitterpanda63 Sep 10, 2025
0af7bb2
Add skip on benchie in CI/CD
bitterpanda63 Sep 10, 2025
d90409c
remove debugging raise e
bitterpanda63 Sep 11, 2025
f57b48a
rename the check to is_attack_wave
bitterpanda63 Sep 11, 2025
d448274
separate concerns in on_detected_attack_wave
bitterpanda63 Sep 11, 2025
9d1ec5d
remove/improve comments
bitterpanda63 Sep 11, 2025
fef7e80
Variable 'methods' is too generic for a security context - should spe…
bitterpanda63 Sep 11, 2025
a43629d
Update comments in is_web_scan_path
bitterpanda63 Sep 11, 2025
f37cf7b
linting
bitterpanda63 Oct 8, 2025
821c98a
Update aikido_zen/background_process/aikido_background_process.py
bitterpanda63 Dec 15, 2025
39f9473
Do run QA tests for attack waves
bitterpanda63 Dec 15, 2025
dba2b84
fix request handler for now
bitterpanda63 Dec 15, 2025
2b4fde1
delete queue_helpers
bitterpanda63 Dec 15, 2025
638a7d2
revert changes made to background_process & check_firewall_lists
bitterpanda63 Dec 15, 2025
26b7a0b
Revert "context, reset cache before save"
bitterpanda63 Dec 15, 2025
a2cbe7f
Update aikido_zen/background_process/cloud_connection_manager/__init_…
bitterpanda63 Dec 15, 2025
15f806a
Add a new function create_attack_wave_event
bitterpanda63 Dec 15, 2025
c82df82
clneaup request_handler
bitterpanda63 Dec 15, 2025
6ff29ff
delete on_detected_attack_wave.py
bitterpanda63 Dec 15, 2025
5cbcb9c
add test cases for create_attack_wave_event
bitterpanda63 Dec 15, 2025
3b9a5b6
linting on create_attack_wave_event_test
bitterpanda63 Dec 15, 2025
db52072
Create a store for the attack wave detector
bitterpanda63 Dec 15, 2025
1b11d0f
Add test cases to the attack wave detector store
bitterpanda63 Dec 15, 2025
c5ced77
request_handler: check for attack wave
bitterpanda63 Dec 15, 2025
7be910c
remove unused is_attack_wave_request code from request_handler
bitterpanda63 Dec 15, 2025
e4b48f8
fix linting violation in check_firewall_lists
bitterpanda63 Dec 15, 2025
5aa29dc
Update aikido_zen/sources/functions/request_handler.py
bitterpanda63 Dec 15, 2025
83399d3
fix request_handler_test test cases
bitterpanda63 Dec 15, 2025
da59957
linting
bitterpanda63 Dec 15, 2025
ac77c31
fix django_mysql e2e test
bitterpanda63 Dec 15, 2025
d1ac363
fix django_mysql_test e2e
bitterpanda63 Dec 15, 2025
cf96fbf
fix request_handler.py to move attack wave to post_response
bitterpanda63 Dec 15, 2025
67c8478
Update request_handler_test test cases
bitterpanda63 Dec 15, 2025
614b0c1
final test case improvements
bitterpanda63 Dec 15, 2025
9ae38d5
fix linting issues
bitterpanda63 Dec 15, 2025
057b6be
Remove weird test
bitterpanda63 Dec 16, 2025
2e18a88
implement node changes
bitterpanda63 Dec 16, 2025
dbb0598
Revert check_firewall_lists changes
bitterpanda63 Dec 16, 2025
436c2e8
revert changes for check_firewall_lists made to request_handler
bitterpanda63 Dec 16, 2025
11b9cfe
remove now broken import in request_handler.py
bitterpanda63 Dec 16, 2025
35d6ffb
Only run s.upper() once
bitterpanda63 Dec 16, 2025
c10bd15
Update aikido_zen/background_process/aikido_background_process.py
bitterpanda63 Dec 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/qa-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ jobs:
dockerfile_path: ./zen-demo-python/Dockerfile
app_port: 8080
sleep_before_test: 10
skip_tests: test_bypassed_ip_for_geo_blocking,test_demo_apps_generic_tests,test_path_traversal,test_wave_attack
skip_tests: test_bypassed_ip_for_geo_blocking,test_demo_apps_generic_tests,test_path_traversal
3 changes: 3 additions & 0 deletions aikido_zen/background_process/commands/sync_data_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def test_process_sync_data_initialization(setup_connection_manager):
assert connection_manager.statistics.get_record()["requests"] == {
"aborted": 0,
"attacksDetected": {"blocked": 0, "total": 5},
"attackWaves": {"total": 0, "blocked": 0},
"total": 10,
"rateLimited": 0,
}
Expand Down Expand Up @@ -168,6 +169,7 @@ def test_process_sync_data_with_last_updated_at_below_zero(setup_connection_mana
assert connection_manager.statistics.get_record()["requests"] == {
"aborted": 0,
"attacksDetected": {"blocked": 0, "total": 5},
"attackWaves": {"total": 0, "blocked": 0},
"total": 10,
"rateLimited": 0,
}
Expand Down Expand Up @@ -255,6 +257,7 @@ def test_process_sync_data_existing_route_and_hostnames(setup_connection_manager
assert connection_manager.statistics.get_record()["requests"] == {
"aborted": 0,
"attacksDetected": {"blocked": 0, "total": 10},
"attackWaves": {"total": 0, "blocked": 0},
"total": 20,
"rateLimited": 0,
}
Expand Down
27 changes: 27 additions & 0 deletions aikido_zen/helpers/create_attack_wave_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from aikido_zen.helpers.limit_length_metadata import limit_length_metadata
from aikido_zen.helpers.logging import logger


def create_attack_wave_event(context, metadata):
try:
return {
"type": "detected_attack_wave",
"attack": {
"user": getattr(context, "user", None),
"metadata": limit_length_metadata(metadata, 4096),
},
"request": extract_request_if_possible(context),
}
except Exception as e:
logger.error("Failed to create detected_attack_wave API event: %s", str(e))
return None


def extract_request_if_possible(context):
if not context:
return None
return {
"ipAddress": getattr(context, "remote_address", None),
"source": getattr(context, "source", None),
"userAgent": context.get_user_agent(),
}
173 changes: 173 additions & 0 deletions aikido_zen/helpers/create_attack_wave_event_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import pytest
from unittest.mock import MagicMock
from .create_attack_wave_event import (
create_attack_wave_event,
extract_request_if_possible,
)
import aikido_zen.test_utils as test_utils


def test_create_attack_wave_event_success():
"""Test successful creation of attack wave event with basic data"""
metadata = {"test": "value"}
context = test_utils.generate_context()

event = create_attack_wave_event(context, metadata)

assert event is not None
assert event["type"] == "detected_attack_wave"
assert event["attack"]["user"] is None
assert event["attack"]["metadata"] == metadata
assert event["request"] is not None


def test_create_attack_wave_event_with_user():
"""Test attack wave event creation with user information"""
metadata = {"test": "value"}
context = test_utils.generate_context(user="test_user")

event = create_attack_wave_event(context, metadata)

assert event["attack"]["user"] == "test_user"
assert event["attack"]["metadata"] == metadata


def test_create_attack_wave_event_with_long_metadata():
"""Test that metadata longer than 4096 characters is truncated"""
long_metadata = "x" * 5000 # Create metadata longer than 4096 characters
metadata = {"test": long_metadata}
context = test_utils.generate_context()

event = create_attack_wave_event(context, metadata)

assert len(event["attack"]["metadata"]["test"]) == 4096
assert event["attack"]["metadata"]["test"] == long_metadata[:4096]


def test_create_attack_wave_event_with_multiple_long_metadata_fields():
"""Test that multiple metadata fields longer than 4096 characters are truncated"""
long_value1 = "a" * 5000
long_value2 = "b" * 6000
metadata = {
"field1": long_value1,
"field2": long_value2,
}
context = test_utils.generate_context()

event = create_attack_wave_event(context, metadata)

assert len(event["attack"]["metadata"]["field1"]) == 4096
assert len(event["attack"]["metadata"]["field2"]) == 4096
assert event["attack"]["metadata"]["field1"] == long_value1[:4096]
assert event["attack"]["metadata"]["field2"] == long_value2[:4096]


def test_create_attack_wave_event_request_data():
"""Test that request data is correctly extracted from context"""
metadata = {"test": "value"}
context = test_utils.generate_context(
ip="198.51.100.23",
route="/test-route",
headers={"user-agent": "Mozilla/5.0"},
)

event = create_attack_wave_event(context, metadata)

request_data = event["request"]
assert request_data["ipAddress"] == "198.51.100.23"
assert request_data["source"] == "flask"
assert request_data["userAgent"] == "Mozilla/5.0"


def test_create_attack_wave_event_no_context():
"""Test attack wave event creation with None context"""
metadata = {"test": "value"}

event = create_attack_wave_event(None, metadata)

assert event["attack"]["user"] is None
assert event["attack"]["metadata"] == metadata
assert event["request"] is None


def test_create_attack_wave_event_exception_handling():
"""Test that exceptions during event creation are handled gracefully"""
# Create a context that will raise an exception when accessed
context = MagicMock()
context.user = "test_user"
context.remote_address = "1.1.1.1"
context.source = "test_source"
# Make get_user_agent raise an exception
context.get_user_agent.side_effect = Exception("Test exception")

metadata = {"test": "value"}

# This should not raise an exception, but return None
event = create_attack_wave_event(context, metadata)

# Since we're mocking and causing an exception, the function should handle it
# and return None based on the exception handling in the function
assert event is None


def test_extract_request_if_possible_with_valid_context():
"""Test request extraction with valid context"""
context = test_utils.generate_context(
ip="198.51.100.23",
route="/test-route",
headers={"user-agent": "Mozilla/5.0"},
)

request = extract_request_if_possible(context)

assert request is not None
assert request["ipAddress"] == "198.51.100.23"
assert request["source"] == "flask"
assert request["userAgent"] == "Mozilla/5.0"


def test_extract_request_if_possible_with_none_context():
"""Test request extraction with None context"""
request = extract_request_if_possible(None)
assert request is None


def test_extract_request_if_possible_with_minimal_context():
"""Test request extraction with minimal context data"""
context = test_utils.generate_context()

request = extract_request_if_possible(context)

assert request is not None
assert request["ipAddress"] == "1.1.1.1"
assert request["source"] == "flask"
assert request["userAgent"] is None


def test_create_attack_wave_event_empty_metadata():
"""Test attack wave event creation with empty metadata"""
metadata = {}
context = test_utils.generate_context()

event = create_attack_wave_event(context, metadata)

assert event is not None
assert event["attack"]["metadata"] == {}
assert event["request"] is not None


def test_create_attack_wave_event_complex_metadata():
"""Test attack wave event creation with complex nested metadata"""
metadata = {
"nested": {"key1": "value1", "key2": "value2"},
"simple": "simple_value",
"json_string": "[1, 2, 3]",
"number_string": "42",
}
context = test_utils.generate_context()

event = create_attack_wave_event(context, metadata)

assert event["attack"]["metadata"] == metadata
assert event["attack"]["metadata"]["nested"]["key1"] == "value1"
assert event["attack"]["metadata"]["json_string"] == "[1, 2, 3]"
7 changes: 4 additions & 3 deletions aikido_zen/ratelimiting/lru_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from collections import OrderedDict
from aikido_zen.helpers.get_current_unixtime_ms import get_unixtime_ms
import aikido_zen.helpers.get_current_unixtime_ms as internal_time


class LRUCache:
Expand All @@ -24,7 +24,8 @@ def get(self, key):
if key in self.cache:
# Check if the item is still valid based on TTL
if (
get_unixtime_ms(monotonic=True) - self.cache[key]["startTime"]
internal_time.get_unixtime_ms(monotonic=True)
- self.cache[key]["startTime"]
< self.time_to_live_in_ms
):
return self.cache[key]["value"] # Return the actual value
Expand All @@ -39,7 +40,7 @@ def set(self, key, value):
self.cache.popitem(last=False) # Remove the oldest item
self.cache[key] = {
"value": value,
"startTime": get_unixtime_ms(monotonic=True),
"startTime": internal_time.get_unixtime_ms(monotonic=True),
} # Store value and timestamp

def clear(self):
Expand Down
30 changes: 24 additions & 6 deletions aikido_zen/sources/functions/request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
from aikido_zen.api_discovery.update_route_info import update_route_info_from_context
from aikido_zen.helpers.is_useful_route import is_useful_route
from aikido_zen.helpers.logging import logger
from aikido_zen.helpers.create_attack_wave_event import create_attack_wave_event
from aikido_zen.thread.thread_cache import get_cache
from .ip_allowed_to_access_route import ip_allowed_to_access_route
import aikido_zen.background_process.comms as c
from ...background_process.commands import PutEventCommand
from ...helpers.ipc.send_payload import send_payload
from ...helpers.serialize_to_json import serialize_to_json
from ...storage.attack_wave_detector_store import attack_wave_detector_store
Copy link
Copy Markdown

@aikido-pr-checks aikido-pr-checks bot Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module-level import attack_wave_detector_store introduces a shared store that may cache request-specific attack data across requests.

Details

✨ AI Reasoning
​​1) The change adds a module-level import of attack_wave_detector_store which creates a shared singleton-like object accessible across requests.
​2) A shared module-level store can unintentionally retain request-specific state and leak data between requests or users, harming isolation and causing race conditions.
​3) The violation is true because the PR introduced use of a module-scoped store for per-request attack detection.
​4) Fixing would reduce risk of cross-request state leakage.
​5) Singleton stores are a common source of such bugs.
​6) This is fixable within the PR by ensuring the store is request-scoped or clearly documented as intended shared cache.

🔧 How do I fix it?
Avoid storing request-specific data in module-level variables. Use request-scoped variables or explicitly mark shared caches as intentional.

More info - Comment @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.



def request_handler(stage, status_code=0):
Expand Down Expand Up @@ -79,25 +84,38 @@ def pre_response():
if block_type == "bot-blocking":
msg = "You are not allowed to access this resource because you have been identified as a bot."
return msg, 403
return None


def post_response(status_code):
"""Checks if the current route is useful"""
"""Checks if the current route is useful and performs attack wave detection"""
context = ctx.get_current_context()
if not context:
return
route_metadata = context.get_route_metadata()

cache = get_cache()
if not cache:
return

attack_wave = attack_wave_detector_store.is_attack_wave(context.remote_address)
Copy link
Copy Markdown

@aikido-pr-checks aikido-pr-checks bot Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to shared attack_wave_detector_store.is_attack_wave from request handler can cause race conditions if the store is not thread-safe

Details

✨ AI Reasoning
​​1) The code calls attack_wave_detector_store.is_attack_wave(context.remote_address) from the request handling path, which likely accesses shared mutable detector state; 2) Accessing a shared detector store from multiple request threads can cause races if the store isn't internally synchronized; 3) This harms correctness because concurrent reads/writes to the detector store can yield stale or inconsistent results; 4) The change introduced this direct call into the request path (line 101 in the diff); 5) This is not a trivial local variable and appears to be a shared store used across threads; 6) Fixing requires ensuring the store provides thread-safe access or protecting usage with synchronization, which is a reasonable change to make in this PR's scope.

🔧 How do I fix it?
Use locks, concurrent collections, or atomic operations when accessing shared mutable state. Avoid modifying collections during iteration. Use proper synchronization primitives like mutex, lock, or thread-safe data structures.

More info - Comment @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.

if attack_wave:
cache.stats.on_detected_attack_wave(blocked=False)

event = create_attack_wave_event(context, metadata={})
logger.debug("Attack wave: %s", serialize_to_json(event)[:5000])

# Report in background to core (send event over IPC)
Copy link
Copy Markdown

@aikido-pr-checks aikido-pr-checks bot Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment '# Report in background to core (send event over IPC)' restates the mechanics of the following code instead of explaining the rationale or intent.

Details

🔧 How do I fix it?
Write comments that explain the purpose, reasoning, or business logic behind the code using words like 'because', 'so that', or 'in order to'.

More info - Comment @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.

if c.get_comms() and event:
send_payload(c.get_comms(), PutEventCommand.generate(event))

# Check if the current route is useful for API discovery
is_curr_route_useful = is_useful_route(
status_code,
context.route,
context.method,
)
if not is_curr_route_useful:
return

cache = get_cache()
if cache:
if is_curr_route_useful:
cache.routes.increment_route(route_metadata)

# api spec generation
Expand Down
Loading
Loading