Skip to content

Commit fbdb1cf

Browse files
authored
Add Security Response ID (#10133)
What Does This Do This PR adds support for extracting and emitting a unique security_response_id (UUIDv4 format) in AppSec blocking responses, generated by libddwaf v17.3.0 Implementation flow: Extraction (WAFModule.java): When libddwaf triggers a blocking action, extract the security_response_id from actionInfo.parameters and pass it to RequestBlockingAction constructor Propagation (Flow.java): Add securityResponseId field to RequestBlockingAction class with getter method and update all constructors (including forRedirect() factory method) Servlet Integration: Update all blocking helper implementations to pass securityResponseId through to template rendering Template Rendering (BlockingActionHelper.java): - Add blockId parameter to getTemplate() method - Replace {security_response_id} placeholder in HTML template - Add security_response_id field in JSON template - Append ?security_response_id=<uuid> to redirect URLs Result: Blocking responses now include the unique identifier in all response types (JSON, HTML, redirect), enabling customers to track and debug specific blocking events. Motivation Per RFC-1070, libddwaf v17.3.0 generates a UUIDv4 as security_response_id in action parameters to provide unique identifiers for each blocking event. This PR implements the required changes: Extract: Retrieve security_response_id from libddwaf action parameters in WAFModule.java Propagate: Add blockId field to Flow.Action.RequestBlockingAction and pass it through all servlet blocking helpers Emit: JSON responses: Include as "security_response_id": "" field HTML responses: Replace {security_response_id} placeholder with actual UUID Redirect responses: Append as URL query parameter ?security_response_id= This enables customers to uniquely identify and track specific blocking events for debugging and analysis.
1 parent cce064e commit fbdb1cf

File tree

51 files changed

+983
-150
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+983
-150
lines changed

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/blocking/BlockingActionHelper.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.io.FileNotFoundException;
1313
import java.io.IOException;
1414
import java.io.InputStream;
15+
import java.nio.charset.StandardCharsets;
1516
import java.util.regex.Matcher;
1617
import java.util.regex.Pattern;
1718
import org.slf4j.Logger;
@@ -118,12 +119,26 @@ public static TemplateType determineTemplateType(
118119
}
119120

120121
public static byte[] getTemplate(TemplateType type) {
122+
return getTemplate(type, null);
123+
}
124+
125+
public static byte[] getTemplate(TemplateType type, String securityResponseId) {
126+
byte[] template;
121127
if (type == TemplateType.JSON) {
122-
return TEMPLATE_JSON;
128+
template = TEMPLATE_JSON;
123129
} else if (type == TemplateType.HTML) {
124-
return TEMPLATE_HTML;
130+
template = TEMPLATE_HTML;
131+
} else {
132+
return null;
125133
}
126-
return null;
134+
135+
// Use empty string when securityResponseId is not present
136+
String replacementValue =
137+
(securityResponseId == null || securityResponseId.isEmpty()) ? "" : securityResponseId;
138+
139+
String templateString = new String(template, StandardCharsets.UTF_8);
140+
String replacedTemplate = templateString.replace("[security_response_id]", replacementValue);
141+
return replacedTemplate.getBytes(StandardCharsets.UTF_8);
127142
}
128143

129144
public static String getContentType(TemplateType type) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
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}</style></head><body><main><p>Sorry, you cannot access this page. Please contact the customer service team.</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+
<!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: [security_response_id]</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>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"errors":[{"title":"You've been blocked","detail":"Sorry, you cannot access this page. Please contact the customer service team. Security provided by Datadog."}]}
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":"[security_response_id]"}

dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/blocking/BlockingActionHelperSpecification.groovy

Lines changed: 96 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -67,104 +67,150 @@ class BlockingActionHelperSpecification extends DDSpecification {
6767
null | null
6868
}
6969

70-
void 'getTemplate returning default html template'() {
70+
void 'getTemplate returning default #templateType template'() {
7171
expect:
72-
new String(BlockingActionHelper.getTemplate(HTML), StandardCharsets.UTF_8)
73-
.contains("<title>You've been blocked</title>")
74-
}
72+
new String(BlockingActionHelper.getTemplate(templateType), StandardCharsets.UTF_8)
73+
.contains(expectedContent)
7574

76-
void 'getTemplate returning default JSON template'() {
77-
expect:
78-
new String(BlockingActionHelper.getTemplate(JSON), StandardCharsets.UTF_8)
79-
.contains('"You\'ve been blocked"')
75+
where:
76+
templateType | expectedContent
77+
HTML | "<title>You've been blocked</title>"
78+
JSON | '"You\'ve been blocked"'
8079
}
8180

82-
void 'getTemplate returning custom html template'() {
81+
void 'getTemplate returning custom #templateType template'() {
8382
setup:
8483
File tempDir = File.createTempDir('testTempDir-', '')
8584
Config config = Mock(Config)
86-
File tempFile = new File(tempDir, 'template.html')
87-
tempFile << '<body>My template</body>'
85+
File tempFile = new File(tempDir, fileName)
86+
tempFile << templateContent
8887

8988
when:
9089
BlockingActionHelper.reset(config)
9190

9291
then:
93-
1 * config.getAppSecHttpBlockedTemplateHtml() >> tempFile.toString()
94-
1 * config.getAppSecHttpBlockedTemplateJson() >> null
95-
new String(BlockingActionHelper.getTemplate(HTML), StandardCharsets.UTF_8)
96-
.contains('<body>My template</body>')
92+
1 * config.getAppSecHttpBlockedTemplateHtml() >> (templateType == HTML ? tempFile.toString() : null)
93+
1 * config.getAppSecHttpBlockedTemplateJson() >> (templateType == JSON ? tempFile.toString() : null)
94+
new String(BlockingActionHelper.getTemplate(templateType), StandardCharsets.UTF_8)
95+
.contains(templateContent)
9796

9897
cleanup:
9998
BlockingActionHelper.reset(Config.get())
10099
tempDir.deleteDir()
100+
101+
where:
102+
templateType | fileName | templateContent
103+
HTML | 'template.html' | '<body>My template</body>'
104+
JSON | 'template.json' | '{"foo":"bar"}'
105+
}
106+
107+
void 'getTemplate with null argument'() {
108+
expect:
109+
BlockingActionHelper.getTemplate(null) == null
101110
}
102111

103-
void 'getTemplate returning custom json template'() {
112+
void 'will use default #templateType template if #reason'() {
104113
setup:
105-
File tempDir = File.createTempDir('testTempDir-', '')
106114
Config config = Mock(Config)
107-
File tempFile = new File(tempDir, 'template.json')
108-
tempFile << '{"foo":"bar"}'
115+
File tempDir = tempDirSetup ? File.createTempDir('testTempDir-', '') : null
116+
File template = tempFile ? new File(tempDir, 'template') : null
117+
if (template) {
118+
template << 'a' * (500 * 1024 + 1)
119+
}
109120

110121
when:
111122
BlockingActionHelper.reset(config)
112123

113124
then:
114-
1 * config.getAppSecHttpBlockedTemplateHtml() >> null
115-
1 * config.getAppSecHttpBlockedTemplateJson() >> tempFile.toString()
116-
new String(BlockingActionHelper.getTemplate(JSON), StandardCharsets.UTF_8)
117-
.contains('{"foo":"bar"}')
125+
1 * config.getAppSecHttpBlockedTemplateHtml() >> htmlConfigValue?.call(template)
126+
1 * config.getAppSecHttpBlockedTemplateJson() >> jsonConfigValue?.call(template)
127+
new String(BlockingActionHelper.getTemplate(templateType), StandardCharsets.UTF_8)
128+
.contains(expectedContent)
118129

119130
cleanup:
120131
BlockingActionHelper.reset(Config.get())
121-
tempDir.deleteDir()
122-
}
132+
if (tempDir) {
133+
tempDir.deleteDir()
134+
}
123135

124-
void 'getTemplate with null argument'() {
125-
expect:
126-
BlockingActionHelper.getTemplate(null) == null
136+
where:
137+
templateType | reason | tempDirSetup | tempFile | htmlConfigValue | jsonConfigValue | expectedContent
138+
HTML | 'custom file does not exist' | false | false | { _ -> '/bad/file.html' } | { _ -> '/bad/file.json' } | "<title>You've been blocked</title>"
139+
JSON | 'custom file does not exist' | false | false | { _ -> '/bad/file.html' } | { _ -> '/bad/file.json' } | '"You\'ve been blocked'
140+
HTML | 'custom file is too big' | true | true | { it -> it.toString() } | { it -> it.toString() } | "<title>You've been blocked</title>"
141+
JSON | 'custom file is too big' | true | true | { it -> it.toString() } | { it -> it.toString() } | '"You\'ve been blocked'
127142
}
128143

129-
void 'will use default templates if custom files do not exist'() {
130-
setup:
131-
Config config = Mock(Config)
144+
145+
void 'getTemplate with security_response_id replaces placeholder in #templateType template'() {
146+
given:
147+
def securityResponseId = '12345678-1234-1234-1234-123456789abc'
132148

133149
when:
134-
BlockingActionHelper.reset(config)
150+
def template = BlockingActionHelper.getTemplate(templateType, securityResponseId)
151+
def templateStr = new String(template, StandardCharsets.UTF_8)
135152

136153
then:
137-
1 * config.getAppSecHttpBlockedTemplateHtml() >> '/bad/file.html'
138-
1 * config.getAppSecHttpBlockedTemplateJson() >> '/bad/file.json'
139-
new String(BlockingActionHelper.getTemplate(HTML), StandardCharsets.UTF_8)
140-
.contains("<title>You've been blocked</title>")
141-
new String(BlockingActionHelper.getTemplate(JSON), StandardCharsets.UTF_8)
142-
.contains('"You\'ve been blocked')
154+
!templateStr.contains('[security_response_id]')
155+
templateStr.contains(expectedContent.replace('[id]', securityResponseId))
143156

144-
cleanup:
145-
BlockingActionHelper.reset(Config.get())
157+
where:
158+
templateType | expectedContent
159+
HTML | 'Security Response ID: [id]'
160+
JSON | '"security_response_id":"[id]"'
161+
}
162+
163+
void 'getTemplate without security_response_id uses empty string in #templateType template'() {
164+
when:
165+
def template = BlockingActionHelper.getTemplate(templateType, null)
166+
def templateStr = new String(template, StandardCharsets.UTF_8)
167+
168+
then:
169+
!templateStr.contains('[security_response_id]')
170+
expectedContents.every { content -> templateStr.contains(content) }
171+
172+
where:
173+
templateType | expectedContents
174+
HTML | ['Security Response ID:']
175+
JSON | ['"security_response_id"', '""']
176+
}
177+
178+
void 'getTemplate with empty security_response_id uses empty string'() {
179+
when:
180+
def htmlTemplate = BlockingActionHelper.getTemplate(HTML, '')
181+
def jsonTemplate = BlockingActionHelper.getTemplate(JSON, '')
182+
183+
then:
184+
!new String(htmlTemplate, StandardCharsets.UTF_8).contains('[security_response_id]')
185+
!new String(jsonTemplate, StandardCharsets.UTF_8).contains('[security_response_id]')
146186
}
147187

148-
void 'will use default templates if custom files are too big'() {
188+
void 'getTemplate with security_response_id works with custom #templateType template'() {
149189
setup:
150-
Config config = Mock(Config)
151190
File tempDir = File.createTempDir('testTempDir-', '')
152-
File template = new File(tempDir, 'template')
153-
template << 'a' * (500 * 1024 + 1)
191+
Config config = Mock(Config)
192+
File tempFile = new File(tempDir, fileName)
193+
tempFile << templateContent
194+
def securityResponseId = 'test-block-id-123'
154195

155196
when:
156197
BlockingActionHelper.reset(config)
198+
def template = BlockingActionHelper.getTemplate(templateType, securityResponseId)
199+
def templateStr = new String(template, StandardCharsets.UTF_8)
157200

158201
then:
159-
1 * config.getAppSecHttpBlockedTemplateHtml() >> template.toString()
160-
1 * config.getAppSecHttpBlockedTemplateJson() >> template.toString()
161-
new String(BlockingActionHelper.getTemplate(HTML), StandardCharsets.UTF_8)
162-
.contains("<title>You've been blocked</title>")
163-
new String(BlockingActionHelper.getTemplate(JSON), StandardCharsets.UTF_8)
164-
.contains('"You\'ve been blocked')
202+
1 * config.getAppSecHttpBlockedTemplateHtml() >> (templateType == HTML ? tempFile.toString() : null)
203+
1 * config.getAppSecHttpBlockedTemplateJson() >> (templateType == JSON ? tempFile.toString() : null)
204+
templateStr.contains(expectedContent.replace('[id]', securityResponseId))
205+
!templateStr.contains('[security_response_id]')
165206

166207
cleanup:
167208
BlockingActionHelper.reset(Config.get())
168209
tempDir.deleteDir()
210+
211+
where:
212+
templateType | fileName | templateContent | expectedContent
213+
HTML | 'template.html' | '<body>Custom template with security_response_id: [security_response_id]</body>' | 'Custom template with security_response_id: [id]'
214+
JSON | 'template.json' | '{"error":"blocked","id":"[security_response_id]"}' | '"error":"blocked","id":"[id]"'
169215
}
170216
}

dd-java-agent/appsec/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dependencies {
1515
implementation project(':internal-api')
1616
implementation project(':communication')
1717
implementation project(':telemetry')
18-
implementation group: 'io.sqreen', name: 'libsqreen', version: '17.2.0'
18+
implementation group: 'io.sqreen', name: 'libsqreen', version: '17.3.0'
1919
implementation libs.moshi
2020

2121
testImplementation libs.bytebuddy

dd-java-agent/appsec/src/main/java/com/datadog/appsec/blocking/BlockingServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public boolean tryCommitBlockingResponse(
8989
log.debug("About to call block response function: {}", blockResponseFunction);
9090
boolean res =
9191
blockResponseFunction.tryCommitBlockingResponse(
92-
reqCtx.getTraceSegment(), statusCode, templateType, extraHeaders);
92+
reqCtx.getTraceSegment(), statusCode, templateType, extraHeaders, null);
9393
if (res) {
9494
TraceSegment traceSegment = reqCtx.getTraceSegment();
9595
if (traceSegment != null) {

0 commit comments

Comments
 (0)