Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,26 @@ public static TemplateType determineTemplateType(
}

public static byte[] getTemplate(TemplateType type) {
return getTemplate(type, null);
}

public static byte[] getTemplate(TemplateType type, String securityResponseId) {
byte[] template;
if (type == TemplateType.JSON) {
return TEMPLATE_JSON;
template = TEMPLATE_JSON;
} else if (type == TemplateType.HTML) {
return TEMPLATE_HTML;
template = TEMPLATE_HTML;
} else {
return null;
}
return null;

// Use empty string when securityResponseId is not present
String replacementValue =
(securityResponseId == null || securityResponseId.isEmpty()) ? "" : securityResponseId;

String templateString = new String(template, java.nio.charset.StandardCharsets.UTF_8);
String replacedTemplate = templateString.replace("[security_response_id]", replacementValue);
return replacedTemplate.getBytes(java.nio.charset.StandardCharsets.UTF_8);
}

public static String getContentType(TemplateType type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
<!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>
Original file line number Diff line number Diff line change
@@ -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."}]}
{"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]"}
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,112 @@ class BlockingActionHelperSpecification extends DDSpecification {
BlockingActionHelper.reset(Config.get())
tempDir.deleteDir()
}


void 'getTemplate with security_response_id replaces placeholder in HTML template'() {
given:
def securityResponseId = '12345678-1234-1234-1234-123456789abc'

when:
def template = BlockingActionHelper.getTemplate(HTML, securityResponseId)
def templateStr = new String(template, StandardCharsets.UTF_8)

then:
templateStr.contains("Security Response ID: ${securityResponseId}")
!templateStr.contains('[security_response_id]')
}

void 'getTemplate with security_response_id replaces placeholder in JSON template'() {
given:
def securityResponseId = '12345678-1234-1234-1234-123456789abc'

when:
def template = BlockingActionHelper.getTemplate(JSON, securityResponseId)
def templateStr = new String(template, StandardCharsets.UTF_8)

then:
templateStr.contains("\"security_response_id\":\"${securityResponseId}\"")
!templateStr.contains('[security_response_id]')
}

void 'getTemplate without security_response_id uses empty string in HTML template'() {
when:
def template = BlockingActionHelper.getTemplate(HTML, null)
def templateStr = new String(template, StandardCharsets.UTF_8)

then:
!templateStr.contains('[security_response_id]')
templateStr.contains('Security Response ID:')
// The placeholder is replaced with empty string
}

void 'getTemplate without security_response_id uses empty string in JSON template'() {
when:
def template = BlockingActionHelper.getTemplate(JSON, null)
def templateStr = new String(template, StandardCharsets.UTF_8)

then:
!templateStr.contains('[security_response_id]')
templateStr.contains('"security_response_id"')
templateStr.contains('""') // Empty string value
}

void 'getTemplate with empty security_response_id uses empty string'() {
when:
def htmlTemplate = BlockingActionHelper.getTemplate(HTML, '')
def jsonTemplate = BlockingActionHelper.getTemplate(JSON, '')

then:
!new String(htmlTemplate, StandardCharsets.UTF_8).contains('[security_response_id]')
!new String(jsonTemplate, StandardCharsets.UTF_8).contains('[security_response_id]')
// Both templates have placeholders replaced with empty string
}

void 'getTemplate with security_response_id works with custom HTML template'() {
setup:
File tempDir = File.createTempDir('testTempDir-', '')
Config config = Mock(Config)
File tempFile = new File(tempDir, 'template.html')
tempFile << '<body>Custom template with security_response_id: [security_response_id]</body>'
def securityResponseId = 'test-block-id-123'

when:
BlockingActionHelper.reset(config)
def template = BlockingActionHelper.getTemplate(HTML, securityResponseId)
def templateStr = new String(template, StandardCharsets.UTF_8)

then:
1 * config.getAppSecHttpBlockedTemplateHtml() >> tempFile.toString()
1 * config.getAppSecHttpBlockedTemplateJson() >> null
templateStr.contains("Custom template with security_response_id: ${securityResponseId}")
!templateStr.contains('[security_response_id]')

cleanup:
BlockingActionHelper.reset(Config.get())
tempDir.deleteDir()
}

void 'getTemplate with security_response_id works with custom JSON template'() {
setup:
File tempDir = File.createTempDir('testTempDir-', '')
Config config = Mock(Config)
File tempFile = new File(tempDir, 'template.json')
tempFile << '{"error":"blocked","id":"[security_response_id]"}'
def securityResponseId = 'test-block-id-456'

when:
BlockingActionHelper.reset(config)
def template = BlockingActionHelper.getTemplate(JSON, securityResponseId)
def templateStr = new String(template, StandardCharsets.UTF_8)

then:
1 * config.getAppSecHttpBlockedTemplateHtml() >> null
1 * config.getAppSecHttpBlockedTemplateJson() >> tempFile.toString()
templateStr.contains("\"error\":\"blocked\",\"id\":\"${securityResponseId}\"")
!templateStr.contains('[security_response_id]')

cleanup:
BlockingActionHelper.reset(Config.get())
tempDir.deleteDir()
}
}
2 changes: 1 addition & 1 deletion dd-java-agent/appsec/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies {
implementation project(':internal-api')
implementation project(':communication')
implementation project(':telemetry')
implementation group: 'io.sqreen', name: 'libsqreen', version: '17.2.0'
implementation group: 'io.sqreen', name: 'libsqreen', version: '17.3.0'
implementation libs.moshi

testImplementation libs.bytebuddy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public boolean tryCommitBlockingResponse(
log.debug("About to call block response function: {}", blockResponseFunction);
boolean res =
blockResponseFunction.tryCommitBlockingResponse(
reqCtx.getTraceSegment(), statusCode, templateType, extraHeaders);
reqCtx.getTraceSegment(), statusCode, templateType, extraHeaders, null);
if (res) {
TraceSegment traceSegment = reqCtx.getTraceSegment();
if (traceSegment != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,19 +363,24 @@ public void onDataAvailable(
WafMetricCollector.get().raspRuleMatch(gwCtx.raspRuleType);
}

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

ActionInfo actionInfo = new ActionInfo(actionType, actionParams);

if ("block_request".equals(actionInfo.type)) {
// Extract security_response_id from action parameters for use in triggers
securityResponseId = (String) actionInfo.parameters.get("security_response_id");
Flow.Action.RequestBlockingAction rba =
createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
createBlockRequestAction(actionInfo, reqCtx, gwCtx.isRasp, securityResponseId);
flow.setAction(rba);
} else if ("redirect_request".equals(actionInfo.type)) {
// Extract security_response_id from action parameters for use in triggers
securityResponseId = (String) actionInfo.parameters.get("security_response_id");
Flow.Action.RequestBlockingAction rba =
createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp);
createRedirectRequestAction(actionInfo, reqCtx, gwCtx.isRasp, securityResponseId);
flow.setAction(rba);
} else if ("generate_stack".equals(actionInfo.type)) {
if (Config.get().isAppSecStackTraceEnabled()) {
Expand Down Expand Up @@ -412,7 +417,7 @@ public void onDataAvailable(
}
}
}
Collection<AppSecEvent> events = buildEvents(resultWithData);
Collection<AppSecEvent> events = buildEvents(resultWithData, securityResponseId);
boolean isThrottled = reqCtx.isThrottled(rateLimiter);

if (!isThrottled) {
Expand Down Expand Up @@ -459,7 +464,10 @@ public void onDataAvailable(
}

private Flow.Action.RequestBlockingAction createBlockRequestAction(
final ActionInfo actionInfo, final AppSecRequestContext reqCtx, final boolean isRasp) {
final ActionInfo actionInfo,
final AppSecRequestContext reqCtx,
final boolean isRasp,
final String securityResponseId) {
try {
int statusCode;
Object statusCodeObj = actionInfo.parameters.get("status_code");
Expand All @@ -477,7 +485,8 @@ private Flow.Action.RequestBlockingAction createBlockRequestAction(
} catch (IllegalArgumentException iae) {
log.warn("Unknown content type: {}; using auto", contentType);
}
return new Flow.Action.RequestBlockingAction(statusCode, blockingContentType);
return new Flow.Action.RequestBlockingAction(
statusCode, blockingContentType, Collections.emptyMap(), securityResponseId);
} catch (RuntimeException cce) {
log.warn("Invalid blocking action data", cce);
if (!isRasp) {
Expand All @@ -488,7 +497,10 @@ private Flow.Action.RequestBlockingAction createBlockRequestAction(
}

private Flow.Action.RequestBlockingAction createRedirectRequestAction(
final ActionInfo actionInfo, final AppSecRequestContext reqCtx, final boolean isRasp) {
final ActionInfo actionInfo,
final AppSecRequestContext reqCtx,
final boolean isRasp,
final String securityResponseId) {
try {
int statusCode;
Object statusCodeObj = actionInfo.parameters.get("status_code");
Expand All @@ -506,6 +518,15 @@ private Flow.Action.RequestBlockingAction createRedirectRequestAction(
if (location == null) {
throw new RuntimeException("redirect_request action has no location");
}
if (securityResponseId != null && !securityResponseId.isEmpty()) {
// For custom redirects, only replace [security_response_id] placeholder if present in the
// URL.
// The client decides whether to include security_response_id by adding the placeholder.
// We don't automatically append security_response_id as a URL parameter.
if (location.contains("[security_response_id]")) {
location = location.replace("[security_response_id]", securityResponseId);
}
}
return Flow.Action.RequestBlockingAction.forRedirect(statusCode, location);
} catch (RuntimeException cce) {
log.warn("Invalid blocking action data", cce);
Expand Down Expand Up @@ -572,7 +593,8 @@ private Waf.ResultWithData runWafTransient(
new DataBundleMapWrapper(ctxAndAddr.addressesOfInterest, newData), LIMITS, metrics);
}

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

if (listResults != null && !listResults.isEmpty()) {
return listResults.stream()
.map(this::buildEvent)
.map(wafResult -> buildEvent(wafResult, securityResponseId))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
return emptyList();
}

private AppSecEvent buildEvent(WAFResultData wafResult) {
private AppSecEvent buildEvent(WAFResultData wafResult, String securityResponseId) {

if (wafResult == null || wafResult.rule == null || wafResult.rule_matches == null) {
log.warn("WAF result is empty: {}", wafResult);
Expand All @@ -615,6 +637,7 @@ private AppSecEvent buildEvent(WAFResultData wafResult) {
.withRuleMatches(wafResult.rule_matches)
.withSpanId(spanId)
.withStackId(wafResult.stack_id)
.withSecurityResponseId(securityResponseId)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public class AppSecEvent {
@com.squareup.moshi.Json(name = "stack_id")
private String stackId;

@com.squareup.moshi.Json(name = "security_response_id")
private String securityResponseId;

public Rule getRule() {
return rule;
}
Expand All @@ -37,6 +40,10 @@ public String getStackId() {
return stackId;
}

public String getSecurityResponseId() {
return securityResponseId;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Expand All @@ -58,6 +65,10 @@ public String toString() {
sb.append("stackId");
sb.append('=');
sb.append(((this.stackId == null) ? "<null>" : this.stackId));
sb.append(',');
sb.append("securityResponseId");
sb.append('=');
sb.append(((this.securityResponseId == null) ? "<null>" : this.securityResponseId));
if (sb.charAt((sb.length() - 1)) == ',') {
sb.setCharAt((sb.length() - 1), ']');
} else {
Expand All @@ -73,6 +84,9 @@ public int hashCode() {
result = ((result * 31) + ((this.ruleMatches == null) ? 0 : this.ruleMatches.hashCode()));
result = ((result * 31) + ((this.spanId == null) ? 0 : this.spanId.hashCode()));
result = ((result * 31) + ((this.stackId == null) ? 0 : this.stackId.hashCode()));
result =
((result * 31)
+ ((this.securityResponseId == null) ? 0 : this.securityResponseId.hashCode()));
return result;
}

Expand All @@ -88,7 +102,8 @@ public boolean equals(Object other) {
return ((Objects.equals(this.rule, rhs.rule))
&& (Objects.equals(this.ruleMatches, rhs.ruleMatches))
&& (Objects.equals(this.spanId, rhs.spanId))
&& (Objects.equals(this.stackId, rhs.stackId)));
&& (Objects.equals(this.stackId, rhs.stackId))
&& (Objects.equals(this.securityResponseId, rhs.securityResponseId)));
}

public static class Builder {
Expand Down Expand Up @@ -125,5 +140,10 @@ public Builder withStackId(String stackId) {
this.instance.stackId = stackId;
return this;
}

public Builder withSecurityResponseId(String securityResponseId) {
this.instance.securityResponseId = securityResponseId;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class BlockingServiceImplSpecification extends DDSpecification {

then:
res == true
1 * brf.tryCommitBlockingResponse(mts, 405, BlockingContentType.HTML, [:],) >> true
1 * brf.tryCommitBlockingResponse(mts, 405, BlockingContentType.HTML, [:], null) >> true
1 * mts.effectivelyBlocked()
}

Expand Down
Loading