Skip to content

Commit d3c9024

Browse files
committed
add security response id
1 parent 4a91aa7 commit d3c9024

File tree

51 files changed

+996
-99
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

+996
-99
lines changed

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,26 @@ public static TemplateType determineTemplateType(
118118
}
119119

120120
public static byte[] getTemplate(TemplateType type) {
121+
return getTemplate(type, null);
122+
}
123+
124+
public static byte[] getTemplate(TemplateType type, String securityResponseId) {
125+
byte[] template;
121126
if (type == TemplateType.JSON) {
122-
return TEMPLATE_JSON;
127+
template = TEMPLATE_JSON;
123128
} else if (type == TemplateType.HTML) {
124-
return TEMPLATE_HTML;
129+
template = TEMPLATE_HTML;
130+
} else {
131+
return null;
125132
}
126-
return null;
133+
134+
// Use empty string when securityResponseId is not present
135+
String replacementValue =
136+
(securityResponseId == null || securityResponseId.isEmpty()) ? "" : securityResponseId;
137+
138+
String templateString = new String(template, java.nio.charset.StandardCharsets.UTF_8);
139+
String replacedTemplate = templateString.replace("[security_response_id]", replacementValue);
140+
return replacedTemplate.getBytes(java.nio.charset.StandardCharsets.UTF_8);
127141
}
128142

129143
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: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,112 @@ class BlockingActionHelperSpecification extends DDSpecification {
167167
BlockingActionHelper.reset(Config.get())
168168
tempDir.deleteDir()
169169
}
170+
171+
172+
void 'getTemplate with security_response_id replaces placeholder in HTML template'() {
173+
given:
174+
def securityResponseId = '12345678-1234-1234-1234-123456789abc'
175+
176+
when:
177+
def template = BlockingActionHelper.getTemplate(HTML, securityResponseId)
178+
def templateStr = new String(template, StandardCharsets.UTF_8)
179+
180+
then:
181+
templateStr.contains("Security Response ID: ${securityResponseId}")
182+
!templateStr.contains('[security_response_id]')
183+
}
184+
185+
void 'getTemplate with security_response_id replaces placeholder in JSON template'() {
186+
given:
187+
def securityResponseId = '12345678-1234-1234-1234-123456789abc'
188+
189+
when:
190+
def template = BlockingActionHelper.getTemplate(JSON, securityResponseId)
191+
def templateStr = new String(template, StandardCharsets.UTF_8)
192+
193+
then:
194+
templateStr.contains("\"security_response_id\":\"${securityResponseId}\"")
195+
!templateStr.contains('[security_response_id]')
196+
}
197+
198+
void 'getTemplate without security_response_id uses empty string in HTML template'() {
199+
when:
200+
def template = BlockingActionHelper.getTemplate(HTML, null)
201+
def templateStr = new String(template, StandardCharsets.UTF_8)
202+
203+
then:
204+
!templateStr.contains('[security_response_id]')
205+
templateStr.contains('Security Response ID:')
206+
// The placeholder is replaced with empty string
207+
}
208+
209+
void 'getTemplate without security_response_id uses empty string in JSON template'() {
210+
when:
211+
def template = BlockingActionHelper.getTemplate(JSON, null)
212+
def templateStr = new String(template, StandardCharsets.UTF_8)
213+
214+
then:
215+
!templateStr.contains('[security_response_id]')
216+
templateStr.contains('"security_response_id"')
217+
templateStr.contains('""') // Empty string value
218+
}
219+
220+
void 'getTemplate with empty security_response_id uses empty string'() {
221+
when:
222+
def htmlTemplate = BlockingActionHelper.getTemplate(HTML, '')
223+
def jsonTemplate = BlockingActionHelper.getTemplate(JSON, '')
224+
225+
then:
226+
!new String(htmlTemplate, StandardCharsets.UTF_8).contains('[security_response_id]')
227+
!new String(jsonTemplate, StandardCharsets.UTF_8).contains('[security_response_id]')
228+
// Both templates have placeholders replaced with empty string
229+
}
230+
231+
void 'getTemplate with security_response_id works with custom HTML template'() {
232+
setup:
233+
File tempDir = File.createTempDir('testTempDir-', '')
234+
Config config = Mock(Config)
235+
File tempFile = new File(tempDir, 'template.html')
236+
tempFile << '<body>Custom template with security_response_id: [security_response_id]</body>'
237+
def securityResponseId = 'test-block-id-123'
238+
239+
when:
240+
BlockingActionHelper.reset(config)
241+
def template = BlockingActionHelper.getTemplate(HTML, securityResponseId)
242+
def templateStr = new String(template, StandardCharsets.UTF_8)
243+
244+
then:
245+
1 * config.getAppSecHttpBlockedTemplateHtml() >> tempFile.toString()
246+
1 * config.getAppSecHttpBlockedTemplateJson() >> null
247+
templateStr.contains("Custom template with security_response_id: ${securityResponseId}")
248+
!templateStr.contains('[security_response_id]')
249+
250+
cleanup:
251+
BlockingActionHelper.reset(Config.get())
252+
tempDir.deleteDir()
253+
}
254+
255+
void 'getTemplate with security_response_id works with custom JSON template'() {
256+
setup:
257+
File tempDir = File.createTempDir('testTempDir-', '')
258+
Config config = Mock(Config)
259+
File tempFile = new File(tempDir, 'template.json')
260+
tempFile << '{"error":"blocked","id":"[security_response_id]"}'
261+
def securityResponseId = 'test-block-id-456'
262+
263+
when:
264+
BlockingActionHelper.reset(config)
265+
def template = BlockingActionHelper.getTemplate(JSON, securityResponseId)
266+
def templateStr = new String(template, StandardCharsets.UTF_8)
267+
268+
then:
269+
1 * config.getAppSecHttpBlockedTemplateHtml() >> null
270+
1 * config.getAppSecHttpBlockedTemplateJson() >> tempFile.toString()
271+
templateStr.contains("\"error\":\"blocked\",\"id\":\"${securityResponseId}\"")
272+
!templateStr.contains('[security_response_id]')
273+
274+
cleanup:
275+
BlockingActionHelper.reset(Config.get())
276+
tempDir.deleteDir()
277+
}
170278
}

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) {

dd-java-agent/appsec/src/main/java/com/datadog/appsec/ddwaf/WAFModule.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -363,19 +363,24 @@ public void onDataAvailable(
363363
WafMetricCollector.get().raspRuleMatch(gwCtx.raspRuleType);
364364
}
365365

366+
String securityResponseId = null;
366367
for (Map.Entry<String, Map<String, Object>> action : resultWithData.actions.entrySet()) {
367368
String actionType = action.getKey();
368369
Map<String, Object> actionParams = action.getValue();
369370

370371
ActionInfo actionInfo = new ActionInfo(actionType, actionParams);
371372

372373
if ("block_request".equals(actionInfo.type)) {
374+
// Extract security_response_id from action parameters for use in triggers
375+
securityResponseId = (String) actionInfo.parameters.get("security_response_id");
373376
Flow.Action.RequestBlockingAction rba =
374-
createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
377+
createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp, securityResponseId);
375378
flow.setAction(rba);
376379
} else if ("redirect_request".equals(actionInfo.type)) {
380+
// Extract security_response_id from action parameters for use in triggers
381+
securityResponseId = (String) actionInfo.parameters.get("security_response_id");
377382
Flow.Action.RequestBlockingAction rba =
378-
createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
383+
createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp, securityResponseId);
379384
flow.setAction(rba);
380385
} else if ("generate_stack".equals(actionInfo.type)) {
381386
if (Config.get().isAppSecStackTraceEnabled()) {
@@ -412,7 +417,7 @@ public void onDataAvailable(
412417
}
413418
}
414419
}
415-
Collection<AppSecEvent> events = buildEvents(resultWithData);
420+
Collection<AppSecEvent> events = buildEvents(resultWithData, securityResponseId);
416421
boolean isThrottled = reqCtx.isThrottled(rateLimiter);
417422

418423
if (!isThrottled) {
@@ -459,7 +464,10 @@ public void onDataAvailable(
459464
}
460465

461466
private Flow.Action.RequestBlockingAction createBlockRequestAction(
462-
final ActionInfo actionInfo, final AppSecRequestContext reqCtx, final boolean isRasp) {
467+
final ActionInfo actionInfo,
468+
final AppSecRequestContext reqCtx,
469+
final boolean isRasp,
470+
final String securityResponseId) {
463471
try {
464472
int statusCode;
465473
Object statusCodeObj = actionInfo.parameters.get("status_code");
@@ -477,7 +485,8 @@ private Flow.Action.RequestBlockingAction createBlockRequestAction(
477485
} catch (IllegalArgumentException iae) {
478486
log.warn("Unknown content type: {}; using auto", contentType);
479487
}
480-
return new Flow.Action.RequestBlockingAction(statusCode, blockingContentType);
488+
return new Flow.Action.RequestBlockingAction(
489+
statusCode, blockingContentType, Collections.emptyMap(), securityResponseId);
481490
} catch (RuntimeException cce) {
482491
log.warn("Invalid blocking action data", cce);
483492
if (!isRasp) {
@@ -488,7 +497,10 @@ private Flow.Action.RequestBlockingAction createBlockRequestAction(
488497
}
489498

490499
private Flow.Action.RequestBlockingAction createRedirectRequestAction(
491-
final ActionInfo actionInfo, final AppSecRequestContext reqCtx, final boolean isRasp) {
500+
final ActionInfo actionInfo,
501+
final AppSecRequestContext reqCtx,
502+
final boolean isRasp,
503+
final String securityResponseId) {
492504
try {
493505
int statusCode;
494506
Object statusCodeObj = actionInfo.parameters.get("status_code");
@@ -506,6 +518,15 @@ private Flow.Action.RequestBlockingAction createRedirectRequestAction(
506518
if (location == null) {
507519
throw new RuntimeException("redirect_request action has no location");
508520
}
521+
if (securityResponseId != null && !securityResponseId.isEmpty()) {
522+
// For custom redirects, only replace [security_response_id] placeholder if present in the
523+
// URL.
524+
// The client decides whether to include security_response_id by adding the placeholder.
525+
// We don't automatically append security_response_id as a URL parameter.
526+
if (location.contains("[security_response_id]")) {
527+
location = location.replace("[security_response_id]", securityResponseId);
528+
}
529+
}
509530
return Flow.Action.RequestBlockingAction.forRedirect(statusCode, location);
510531
} catch (RuntimeException cce) {
511532
log.warn("Invalid blocking action data", cce);
@@ -572,7 +593,8 @@ private Waf.ResultWithData runWafTransient(
572593
new DataBundleMapWrapper(ctxAndAddr.addressesOfInterest, newData), LIMITS, metrics);
573594
}
574595

575-
private Collection<AppSecEvent> buildEvents(Waf.ResultWithData actionWithData) {
596+
private Collection<AppSecEvent> buildEvents(
597+
Waf.ResultWithData actionWithData, String securityResponseId) {
576598
if (actionWithData.data == null) {
577599
log.debug(SEND_TELEMETRY, "WAF result data is null");
578600
return Collections.emptyList();
@@ -590,14 +612,14 @@ private Collection<AppSecEvent> buildEvents(Waf.ResultWithData actionWithData) {
590612

591613
if (listResults != null && !listResults.isEmpty()) {
592614
return listResults.stream()
593-
.map(this::buildEvent)
615+
.map(wafResult -> buildEvent(wafResult, securityResponseId))
594616
.filter(Objects::nonNull)
595617
.collect(Collectors.toList());
596618
}
597619
return emptyList();
598620
}
599621

600-
private AppSecEvent buildEvent(WAFResultData wafResult) {
622+
private AppSecEvent buildEvent(WAFResultData wafResult, String securityResponseId) {
601623

602624
if (wafResult == null || wafResult.rule == null || wafResult.rule_matches == null) {
603625
log.warn("WAF result is empty: {}", wafResult);
@@ -615,6 +637,7 @@ private AppSecEvent buildEvent(WAFResultData wafResult) {
615637
.withRuleMatches(wafResult.rule_matches)
616638
.withSpanId(spanId)
617639
.withStackId(wafResult.stack_id)
640+
.withSecurityResponseId(securityResponseId)
618641
.build();
619642
}
620643

dd-java-agent/appsec/src/main/java/com/datadog/appsec/report/AppSecEvent.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public class AppSecEvent {
2121
@com.squareup.moshi.Json(name = "stack_id")
2222
private String stackId;
2323

24+
@com.squareup.moshi.Json(name = "security_response_id")
25+
private String securityResponseId;
26+
2427
public Rule getRule() {
2528
return rule;
2629
}
@@ -37,6 +40,10 @@ public String getStackId() {
3740
return stackId;
3841
}
3942

43+
public String getSecurityResponseId() {
44+
return securityResponseId;
45+
}
46+
4047
@Override
4148
public String toString() {
4249
StringBuilder sb = new StringBuilder();
@@ -58,6 +65,10 @@ public String toString() {
5865
sb.append("stackId");
5966
sb.append('=');
6067
sb.append(((this.stackId == null) ? "<null>" : this.stackId));
68+
sb.append(',');
69+
sb.append("securityResponseId");
70+
sb.append('=');
71+
sb.append(((this.securityResponseId == null) ? "<null>" : this.securityResponseId));
6172
if (sb.charAt((sb.length() - 1)) == ',') {
6273
sb.setCharAt((sb.length() - 1), ']');
6374
} else {
@@ -73,6 +84,9 @@ public int hashCode() {
7384
result = ((result * 31) + ((this.ruleMatches == null) ? 0 : this.ruleMatches.hashCode()));
7485
result = ((result * 31) + ((this.spanId == null) ? 0 : this.spanId.hashCode()));
7586
result = ((result * 31) + ((this.stackId == null) ? 0 : this.stackId.hashCode()));
87+
result =
88+
((result * 31)
89+
+ ((this.securityResponseId == null) ? 0 : this.securityResponseId.hashCode()));
7690
return result;
7791
}
7892

@@ -88,7 +102,8 @@ public boolean equals(Object other) {
88102
return ((Objects.equals(this.rule, rhs.rule))
89103
&& (Objects.equals(this.ruleMatches, rhs.ruleMatches))
90104
&& (Objects.equals(this.spanId, rhs.spanId))
91-
&& (Objects.equals(this.stackId, rhs.stackId)));
105+
&& (Objects.equals(this.stackId, rhs.stackId))
106+
&& (Objects.equals(this.securityResponseId, rhs.securityResponseId)));
92107
}
93108

94109
public static class Builder {
@@ -125,5 +140,10 @@ public Builder withStackId(String stackId) {
125140
this.instance.stackId = stackId;
126141
return this;
127142
}
143+
144+
public Builder withSecurityResponseId(String securityResponseId) {
145+
this.instance.securityResponseId = securityResponseId;
146+
return this;
147+
}
128148
}
129149
}

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/blocking/BlockingServiceImplSpecification.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class BlockingServiceImplSpecification extends DDSpecification {
108108

109109
then:
110110
res == true
111-
1 * brf.tryCommitBlockingResponse(mts, 405, BlockingContentType.HTML, [:],) >> true
111+
1 * brf.tryCommitBlockingResponse(mts, 405, BlockingContentType.HTML, [:], null) >> true
112112
1 * mts.effectivelyBlocked()
113113
}
114114

0 commit comments

Comments
 (0)