Skip to content

Commit 8b3df9f

Browse files
jandro996devflow.devflow-routing-intake
andcommitted
Missing http.response.headers.content-type span tag on blocking responses (#10711)
fix(appsec): record blocking response content-type centrally in GatewayBridge When a WAF blocking action fires, the normal response-header IG callbacks are bypassed, so http.response.headers.content-type never reaches the span. Instead of patching every framework's blocking handler, intercept the blocking flow result in GatewayBridge.maybePublishRequestData / maybePublishResponseData, compute the deterministic content-type from RequestBlockingAction + accept header, store it on AppSecRequestContext, and write it as a span tag in onRequestEnded(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Merge branch 'master' into alejandro.gonzalez/APPSEC-61447-bug-blocking Fix and more tests Fix and more tests Fix and more tests Merge branch 'master' into alejandro.gonzalez/APPSEC-61447-bug-blocking Merge branch 'master' into alejandro.gonzalez/APPSEC-61447-bug-blocking Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 571c573 commit 8b3df9f

File tree

4 files changed

+116
-2
lines changed

4 files changed

+116
-2
lines changed

dd-java-agent/appsec/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ dependencies {
1919
implementation group: 'io.sqreen', name: 'libsqreen', version: '17.3.0'
2020
implementation libs.moshi
2121

22+
compileOnly project(':dd-java-agent:agent-bootstrap')
23+
testImplementation project(':dd-java-agent:agent-bootstrap')
2224
testImplementation libs.bytebuddy
2325
testImplementation project(':remote-config:remote-config-core')
2426
testImplementation project(':utils:test-utils')

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/AppSecRequestContext.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ public class AppSecRequestContext implements DataBundle, Closeable {
162162
private final AtomicInteger raspMetricsCounter = new AtomicInteger(0);
163163

164164
private volatile boolean wafBlocked;
165+
private volatile String blockingResponseContentType;
166+
private volatile Integer blockingResponseContentLength;
165167
private volatile boolean wafErrors;
166168
private volatile boolean wafTruncated;
167169
private volatile boolean wafRequestBlockFailure;
@@ -237,6 +239,22 @@ public boolean isWafBlocked() {
237239
return wafBlocked;
238240
}
239241

242+
public void setBlockingResponseContentType(String contentType) {
243+
this.blockingResponseContentType = contentType;
244+
}
245+
246+
public String getBlockingResponseContentType() {
247+
return blockingResponseContentType;
248+
}
249+
250+
public void setBlockingResponseContentLength(Integer contentLength) {
251+
this.blockingResponseContentLength = contentLength;
252+
}
253+
254+
public Integer getBlockingResponseContentLength() {
255+
return blockingResponseContentLength;
256+
}
257+
240258
public void setWafErrors() {
241259
this.wafErrors = true;
242260
}

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.datadog.appsec.report.AppSecEvent;
2727
import com.datadog.appsec.report.AppSecEventWrapper;
2828
import com.datadog.appsec.util.BodyParser;
29+
import datadog.appsec.api.blocking.BlockingContentType;
2930
import datadog.trace.api.Config;
3031
import datadog.trace.api.ProductTraceSource;
3132
import datadog.trace.api.appsec.HttpClientPayload;
@@ -42,6 +43,7 @@
4243
import datadog.trace.api.telemetry.LoginEvent;
4344
import datadog.trace.api.telemetry.RuleType;
4445
import datadog.trace.api.telemetry.WafMetricCollector;
46+
import datadog.trace.bootstrap.blocking.BlockingActionHelper;
4547
import datadog.trace.bootstrap.instrumentation.api.Tags;
4648
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
4749
import datadog.trace.util.stacktrace.StackTraceEvent;
@@ -929,6 +931,18 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
929931
writeResponseHeaders(
930932
ctx, traceSeg, RESPONSE_HEADERS_ALLOW_LIST, ctx.getResponseHeaders(), false);
931933
}
934+
// For blocking responses the normal response-header collection is bypassed; write the
935+
// content-type and content-length that were determined when the blocking action was raised.
936+
String blockingContentType = ctx.getBlockingResponseContentType();
937+
if (blockingContentType != null) {
938+
traceSeg.setTagTop("http.response.headers.content-type", blockingContentType);
939+
Integer blockingContentLength = ctx.getBlockingResponseContentLength();
940+
if (blockingContentLength != null) {
941+
traceSeg.setTagTop(
942+
"http.response.headers.content-length", String.valueOf(blockingContentLength));
943+
}
944+
}
945+
932946
// If extracted any derivatives - commit them
933947
if (!ctx.commitDerivatives(traceSeg)) {
934948
log.debug("Unable to commit, derivatives will be skipped {}", ctx.getDerivativeKeys());
@@ -1230,7 +1244,9 @@ private Flow<Void> maybePublishRequestData(AppSecRequestContext ctx) {
12301244

12311245
try {
12321246
GatewayContext gwCtx = new GatewayContext(false);
1233-
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
1247+
Flow<Void> flow = producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
1248+
maybeRecordBlockingContentType(ctx, flow);
1249+
return flow;
12341250
} catch (ExpiredSubscriberInfoException e) {
12351251
this.initialReqDataSubInfo = null;
12361252
}
@@ -1263,7 +1279,9 @@ private Flow<Void> maybePublishResponseData(AppSecRequestContext ctx) {
12631279

12641280
try {
12651281
GatewayContext gwCtx = new GatewayContext(false);
1266-
return producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
1282+
Flow<Void> flow = producerService.publishDataEvent(subInfo, ctx, bundle, gwCtx);
1283+
maybeRecordBlockingContentType(ctx, flow);
1284+
return flow;
12671285
} catch (ExpiredSubscriberInfoException e) {
12681286
respDataSubInfo = null;
12691287
}
@@ -1277,6 +1295,26 @@ private ApiSecurityDownstreamSampler downstreamSampler() {
12771295
return downstreamSampler;
12781296
}
12791297

1298+
private static void maybeRecordBlockingContentType(AppSecRequestContext ctx, Flow<?> flow) {
1299+
Flow.Action action = flow.getAction();
1300+
if (!(action instanceof Flow.Action.RequestBlockingAction)) {
1301+
return;
1302+
}
1303+
Flow.Action.RequestBlockingAction rba = (Flow.Action.RequestBlockingAction) action;
1304+
BlockingContentType bct = rba.getBlockingContentType();
1305+
if (bct == BlockingContentType.NONE) {
1306+
return; // redirect — no response body
1307+
}
1308+
List<String> acceptValues = ctx.getRequestHeaders().get("accept");
1309+
String acceptHeader =
1310+
(acceptValues == null || acceptValues.isEmpty()) ? null : acceptValues.get(0);
1311+
BlockingActionHelper.TemplateType tt =
1312+
BlockingActionHelper.determineTemplateType(bct, acceptHeader);
1313+
byte[] template = BlockingActionHelper.getTemplate(tt, rba.getSecurityResponseId());
1314+
ctx.setBlockingResponseContentType(BlockingActionHelper.getContentType(tt));
1315+
ctx.setBlockingResponseContentLength(template.length);
1316+
}
1317+
12801318
private static Map<String, List<String>> parseQueryStringParams(
12811319
String queryString, Charset uriEncoding) {
12821320
if (queryString == null) {

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import datadog.trace.api.appsec.MediaType
1818
import datadog.trace.api.config.GeneralConfig
1919
import datadog.trace.api.function.TriConsumer
2020
import datadog.trace.api.function.TriFunction
21+
import datadog.appsec.api.blocking.BlockingContentType
22+
import datadog.trace.bootstrap.blocking.BlockingActionHelper
2123
import datadog.trace.api.gateway.BlockResponseFunction
2224
import datadog.trace.api.gateway.Flow
2325
import datadog.trace.api.gateway.IGSpanInfo
@@ -1637,6 +1639,60 @@ class GatewayBridgeSpecification extends DDSpecification {
16371639
}
16381640
}
16391641
1642+
void 'blocking response content-type span tag is written for AUTO bct resolved to application/json'() {
1643+
setup:
1644+
def rba = new Flow.Action.RequestBlockingAction(403, BlockingContentType.AUTO)
1645+
def blockingFlow = [getAction: {
1646+
rba
1647+
}, getResult: {
1648+
null
1649+
}] as Flow
1650+
IGSpanInfo spanInfo = Stub(AgentSpan) {
1651+
getTags() >> TagMap.fromMap([:])
1652+
}
1653+
1654+
when:
1655+
requestMethodURICB.apply(ctx, 'GET', TestURIDataAdapter.create('/'))
1656+
reqHeaderCB.accept(ctx, 'accept', 'application/json')
1657+
reqHeadersDoneCB.apply(ctx)
1658+
eventDispatcher.getDataSubscribers(_) >> nonEmptyDsInfo
1659+
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> blockingFlow
1660+
requestSocketAddressCB.apply(ctx, '0.0.0.0', 5555)
1661+
requestEndedCB.apply(ctx, spanInfo)
1662+
1663+
then:
1664+
1 * traceSegment.setTagTop('http.response.headers.content-type', 'application/json')
1665+
1 * traceSegment.setTagTop('http.response.headers.content-length',
1666+
String.valueOf(BlockingActionHelper.getTemplate(BlockingActionHelper.TemplateType.JSON, null).length))
1667+
}
1668+
1669+
void 'blocking response content-type span tag is written for AUTO bct resolved to text/html'() {
1670+
setup:
1671+
def rba = new Flow.Action.RequestBlockingAction(403, BlockingContentType.AUTO)
1672+
def blockingFlow = [getAction: {
1673+
rba
1674+
}, getResult: {
1675+
null
1676+
}] as Flow
1677+
IGSpanInfo spanInfo = Stub(AgentSpan) {
1678+
getTags() >> TagMap.fromMap([:])
1679+
}
1680+
1681+
when:
1682+
requestMethodURICB.apply(ctx, 'GET', TestURIDataAdapter.create('/'))
1683+
reqHeaderCB.accept(ctx, 'accept', 'text/html,application/xhtml+xml')
1684+
reqHeadersDoneCB.apply(ctx)
1685+
eventDispatcher.getDataSubscribers(_) >> nonEmptyDsInfo
1686+
eventDispatcher.publishDataEvent(nonEmptyDsInfo, ctx.data, _ as DataBundle, _ as GatewayContext) >> blockingFlow
1687+
requestSocketAddressCB.apply(ctx, '0.0.0.0', 5555)
1688+
requestEndedCB.apply(ctx, spanInfo)
1689+
1690+
then:
1691+
1 * traceSegment.setTagTop('http.response.headers.content-type', 'text/html;charset=utf-8')
1692+
1 * traceSegment.setTagTop('http.response.headers.content-length',
1693+
String.valueOf(BlockingActionHelper.getTemplate(BlockingActionHelper.TemplateType.HTML, null).length))
1694+
}
1695+
16401696
static toLowerCaseHeaders(final Map<String, List<String>> headers) {
16411697
return headers.collectEntries {
16421698
[(it.key.toLowerCase(Locale.ROOT)): it.value]

0 commit comments

Comments
 (0)