Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions manifests/cpp_nginx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ tests/:
Test_Blocking: v1.2.0
Test_Blocking_strip_response_headers: irrelevant (no response headers on 1st waf run, which is where blocking is possible)
Test_CustomBlockingResponse: v1.2.0
test_blocking_security_response_id.py:
Test_SecurityResponseId_Custom_Redirect: missing_feature
Test_SecurityResponseId_HTML_Response: missing_feature
Test_SecurityResponseId_In_Span_Triggers: missing_feature
Test_SecurityResponseId_JSON_Response: missing_feature
test_custom_rules.py:
Test_CustomRules: v1.2.0
test_exclusions.py:
Expand Down
5 changes: 5 additions & 0 deletions manifests/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ tests/:
Test_Blocking: v2.27.0
Test_Blocking_strip_response_headers: missing_feature
Test_CustomBlockingResponse: v3.10.0
test_blocking_security_response_id.py:
Test_SecurityResponseId_Custom_Redirect: missing_feature
Test_SecurityResponseId_HTML_Response: missing_feature
Test_SecurityResponseId_In_Span_Triggers: missing_feature
Test_SecurityResponseId_JSON_Response: missing_feature
test_custom_rules.py:
Test_CustomRules: v2.30.0
test_exclusions.py:
Expand Down
5 changes: 5 additions & 0 deletions manifests/golang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ tests/:
Test_Blocking: v1.50.0-rc.1
Test_Blocking_strip_response_headers: missing_feature
Test_CustomBlockingResponse: v1.63.0
test_blocking_security_response_id.py:
Test_SecurityResponseId_Custom_Redirect: missing_feature
Test_SecurityResponseId_HTML_Response: missing_feature
Test_SecurityResponseId_In_Span_Triggers: missing_feature
Test_SecurityResponseId_JSON_Response: missing_feature
test_custom_rules.py:
Test_CustomRules: v1.51.0
test_exclusions.py:
Expand Down
13 changes: 13 additions & 0 deletions manifests/java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,19 @@ tests/:
akka-http: v1.22.0
play: v1.22.0
spring-boot-3-native: irrelevant (GraalVM. Tracing support only)
test_blocking_security_response_id.py:
Test_SecurityResponseId_Custom_Redirect:
'*': missing_feature
spring-boot-3-native: irrelevant (GraalVM. Tracing support only)
Test_SecurityResponseId_HTML_Response:
'*': missing_feature
spring-boot-3-native: irrelevant (GraalVM. Tracing support only)
Test_SecurityResponseId_In_Span_Triggers:
'*': missing_feature
spring-boot-3-native: irrelevant (GraalVM. Tracing support only)
Test_SecurityResponseId_JSON_Response:
'*': missing_feature
spring-boot-3-native: irrelevant (GraalVM. Tracing support only)
test_custom_rules.py:
Test_CustomRules:
'*': v1.51.0
Expand Down
5 changes: 5 additions & 0 deletions manifests/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,11 @@ tests/:
Test_Blocking: *ref_3_19_0
Test_Blocking_strip_response_headers: *ref_5_17_0
Test_CustomBlockingResponse: *ref_5_15_0
test_blocking_security_response_id.py:
Test_SecurityResponseId_Custom_Redirect: missing_feature
Test_SecurityResponseId_HTML_Response: missing_feature
Test_SecurityResponseId_In_Span_Triggers: missing_feature
Test_SecurityResponseId_JSON_Response: missing_feature
test_custom_rules.py:
Test_CustomRules:
'*': *ref_4_1_0
Expand Down
5 changes: 5 additions & 0 deletions manifests/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ tests/:
Test_Blocking: missing_feature # v0.86.0
Test_Blocking_strip_response_headers: missing_feature
Test_CustomBlockingResponse: missing_feature # v0.86.0
test_blocking_security_response_id.py:
Test_SecurityResponseId_Custom_Redirect: missing_feature
Test_SecurityResponseId_HTML_Response: missing_feature
Test_SecurityResponseId_In_Span_Triggers: missing_feature
Test_SecurityResponseId_JSON_Response: missing_feature
test_custom_rules.py:
Test_CustomRules: v0.87.2
test_exclusions.py:
Expand Down
5 changes: 5 additions & 0 deletions manifests/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,11 @@ tests/:
Test_CustomBlockingResponse:
'*': v1.20.0
fastapi: v2.4.0
test_blocking_security_response_id.py:
Test_SecurityResponseId_Custom_Redirect: missing_feature
Test_SecurityResponseId_HTML_Response: missing_feature
Test_SecurityResponseId_In_Span_Triggers: missing_feature
Test_SecurityResponseId_JSON_Response: missing_feature
test_custom_rules.py:
Test_CustomRules:
'*': v1.16.1
Expand Down
5 changes: 5 additions & 0 deletions manifests/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,11 @@ tests/:
Test_Blocking: v1.11.0
Test_Blocking_strip_response_headers: v1.13.0
Test_CustomBlockingResponse: v1.15.0
test_blocking_security_response_id.py:
Test_SecurityResponseId_Custom_Redirect: missing_feature
Test_SecurityResponseId_HTML_Response: missing_feature
Test_SecurityResponseId_In_Span_Triggers: missing_feature
Test_SecurityResponseId_JSON_Response: missing_feature
test_custom_rules.py:
Test_CustomRules: v1.12.0
test_exclusions.py:
Expand Down
35 changes: 35 additions & 0 deletions tests/appsec/blocking_rule.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,33 @@
"block4"
]
},
{
"id": "canary_rule5",
"name": "Canary 5",
"tags": {
"type": "security_scanner",
"category": "attack_attempt"
},
"conditions": [
{
"parameters": {
"inputs": [
{
"address": "server.request.headers.no_cookies",
"key_path": [
"user-agent"
]
}
],
"regex": "^Canary\\/v5"
},
"operator": "match_regex"
}
],
"on_match": [
"block5"
]
},
{
"id": "tst-037-009",
"name": "Test block on response header",
Expand Down Expand Up @@ -857,6 +884,14 @@
"status_code": 303,
"location": ""
}
},
{
"id": "block5",
"type": "redirect_request",
"parameters": {
"status_code": 301,
"location": "/you-have-been-blocked?security_response_id=[security_response_id]"
}
}
],
"rules_data": [
Expand Down
1 change: 1 addition & 0 deletions tests/appsec/waf/blocked.v3.min.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>You've been blocked</title><style>a,body,div,html,span{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}body{background:-webkit-radial-gradient(26% 19%,circle,#fff,#f4f7f9);background:radial-gradient(circle at 26% 19%,#fff,#f4f7f9);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-line-pack:center;align-content:center;width:100%;min-height:100vh;line-height:1;flex-direction:column}p{display:block}main{text-align:center;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-line-pack:center;align-content:center;flex-direction:column}p{font-size:18px;line-height:normal;color:#646464;font-family:sans-serif;font-weight:400}a{color:#4842b7}footer{width:100%;text-align:center}footer p{font-size:16px}.security-response-id{font-size:14px;color:#999;margin-top:20px;font-family:monospace}</style></head><body><main><p>Sorry, you cannot access this page. Please contact the customer service team.</p><p class="security-response-id">Security Response ID: 00000000-0000-4000-8000-000000000000</p></main><footer><p>Security provided by <a href="https://www.datadoghq.com/product/security-platform/application-security-monitoring/" target="_blank">Datadog</a></p></footer></body></html>
1 change: 1 addition & 0 deletions tests/appsec/waf/blocked.v3.min.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"errors":[{"title":"You've been blocked","detail":"Sorry, you cannot access this page. Please contact the customer service team. Security provided by Datadog."}],"security_response_id":"00000000-0000-4000-8000-000000000000"}
76 changes: 70 additions & 6 deletions tests/appsec/waf/test_blocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,58 @@

from utils import interfaces, bug, scenarios, weblog, rfc, missing_feature, flaky, features
from utils._context.core import context
from .test_blocking_security_response_id import (
is_valid_uuid4,
extract_security_response_id_from_json,
extract_security_response_id_from_html,
)


BLOCK_TEMPLATE_JSON_MIN_V1 = "blocked.v1.min.json"
BLOCK_TEMPLATE_HTML_MIN_V2 = "blocked.v2.min.html"
BLOCK_TEMPLATE_JSON_MIN_V3 = "blocked.v3.min.json"
BLOCK_TEMPLATE_HTML_MIN_V3 = "blocked.v3.min.html"


def _read_file(file_path: str) -> str:
with Path(__file__).resolve().parent.joinpath(file_path).open() as file:
return file.read()


def _is_valid_json_v3_template(body: str) -> bool:
"""Check if body matches v3 JSON template with valid dynamic security_response_id

RFC-1070: Uses the actual security_response_id from the response for validation
"""
# Extract and validate security_response_id from actual response
security_response_id = extract_security_response_id_from_json(body)
if security_response_id is None or not is_valid_uuid4(security_response_id):
return False

# Build expected response by injecting the actual security_response_id into the template
v3_template = _read_file(BLOCK_TEMPLATE_JSON_MIN_V3).rstrip()
expected_response = v3_template.replace("00000000-0000-4000-8000-000000000000", security_response_id)

return body.rstrip() == expected_response


def _is_valid_html_v3_template(body: str) -> bool:
"""Check if body matches v3 HTML template with valid dynamic security_response_id

RFC-1070: Uses the actual security_response_id from the response for validation
"""
# Extract and validate security_response_id from actual response
security_response_id = extract_security_response_id_from_html(body)
if security_response_id is None or not is_valid_uuid4(security_response_id):
return False

# Build expected response by injecting the actual security_response_id into the template
v3_template = _read_file(BLOCK_TEMPLATE_HTML_MIN_V3).rstrip()
expected_response = v3_template.replace("00000000-0000-4000-8000-000000000000", security_response_id)

return body.rstrip() == expected_response


def assert_valid_html_blocked_template(body: str) -> None:
"""Returns true if body is a valid HTML response on a blocked requests"""

Expand All @@ -24,7 +65,8 @@ def assert_valid_html_blocked_template(body: str) -> None:
_read_file(BLOCK_TEMPLATE_HTML_MIN_V2),
}

assert body in valid_templates
# Check for v3 template with dynamic security_response_id
assert body in valid_templates or _is_valid_html_v3_template(body)


def assert_valid_json_blocked_template(body: str) -> None:
Expand All @@ -38,7 +80,9 @@ def assert_valid_json_blocked_template(body: str) -> None:
_read_file(BLOCK_TEMPLATE_JSON_MIN_V1),
_read_file(BLOCK_TEMPLATE_JSON_MIN_V1).rstrip(),
}
assert body in valid_templates

# Check for v3 template with dynamic security_response_id
assert body in valid_templates or _is_valid_json_v3_template(body)


HTML_CONTENT_TYPES = {"text/html", "text/html; charset=utf-8", "text/html;charset=utf-8"}
Expand Down Expand Up @@ -182,10 +226,20 @@ def setup_json_template_v1(self):
@missing_feature(context.library < "python@2.11.0.dev")
@missing_feature(library="ruby")
def test_json_template_v1(self):
"""HTML block template is v1 minified"""
"""JSON block template is v1 minified (or v3 with security_response_id)"""
assert self.r_json_v1.status_code == 403
assert self.r_json_v1.headers.get("content-type", "").lower() in JSON_CONTENT_TYPES
assert self.r_json_v1.text.rstrip() == _read_file(BLOCK_TEMPLATE_JSON_MIN_V1).rstrip()

# Accept v1 template without security_response_id or v3 template with security_response_id
response_text = self.r_json_v1.text.rstrip()
v1_template = _read_file(BLOCK_TEMPLATE_JSON_MIN_V1).rstrip()

# Check if it's v1 template
if response_text == v1_template:
return

# Check if it's v3 template with valid security_response_id
assert _is_valid_json_v3_template(self.r_json_v1.text), "Response doesn't match v1 or v3 template"

def setup_html_template_v2(self):
self.r_html_v2 = weblog.get("/waf/", headers={"User-Agent": "Arachni/v1", "Accept": "text/html"})
Expand All @@ -197,10 +251,20 @@ def setup_html_template_v2(self):
@missing_feature(context.library < "python@2.11.0.dev")
@missing_feature(library="ruby")
def test_html_template_v2(self):
"""HTML block template is v2 minified"""
"""HTML block template is v2 minified (or v3 with security_response_id)"""
assert self.r_html_v2.status_code == 403
assert self.r_html_v2.headers.get("content-type", "").lower() in HTML_CONTENT_TYPES
assert self.r_html_v2.text == _read_file(BLOCK_TEMPLATE_HTML_MIN_V2)

# Accept v2 template without security_response_id or v3 template with security_response_id
response_text = self.r_html_v2.text
v2_template = _read_file(BLOCK_TEMPLATE_HTML_MIN_V2)

# Check if it's v2 template
if response_text == v2_template:
return

# Check if it's v3 template with valid security_response_id
assert _is_valid_html_v3_template(self.r_html_v2.text), "Response doesn't match v2 or v3 template"


@rfc("https://datadoghq.atlassian.net/wiki/spaces/APS/pages/2705464728/Blocking#Stripping-response-headers")
Expand Down
Loading
Loading