Skip to content

Commit f6f57a6

Browse files
Always collect accept, content-type and user-agent when appsec is enabled (#7009)
1 parent 0946fa5 commit f6f57a6

File tree

3 files changed

+89
-44
lines changed

3 files changed

+89
-44
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public class AppSecRequestContext implements DataBundle, Closeable {
4949
"accept-encoding",
5050
"accept-language"));
5151

52+
// request headers that will always be set when appsec is enabled
53+
public static final Set<String> DEFAULT_REQUEST_HEADERS_ALLOW_LIST =
54+
new TreeSet<>(Arrays.asList("accept", "content-type", "user-agent"));
55+
5256
private final ConcurrentHashMap<Address<?>, Object> persistentData = new ConcurrentHashMap<>();
5357
private Collection<AppSecEvent> collectedEvents; // guarded by this
5458

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

Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.datadog.appsec.gateway;
22

33
import static com.datadog.appsec.event.data.MapDataBundle.Builder.CAPACITY_6_10;
4+
import static com.datadog.appsec.gateway.AppSecRequestContext.DEFAULT_REQUEST_HEADERS_ALLOW_LIST;
5+
import static com.datadog.appsec.gateway.AppSecRequestContext.HEADERS_ALLOW_LIST;
46

57
import com.datadog.appsec.AppSecSystem;
68
import com.datadog.appsec.api.security.ApiSecurityRequestSampler;
@@ -31,7 +33,6 @@
3133
import datadog.trace.api.telemetry.WafMetricCollector;
3234
import datadog.trace.bootstrap.instrumentation.api.Tags;
3335
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
34-
import datadog.trace.util.Strings;
3536
import java.net.URI;
3637
import java.net.URISyntaxException;
3738
import java.nio.charset.Charset;
@@ -130,51 +131,34 @@ public void init() {
130131
pp.processTraceSegment(traceSeg, ctx, collectedEvents);
131132
}
132133

133-
// If detected any events - mark span at appsec.event
134-
if (!collectedEvents.isEmpty() && (rateLimiter == null || !rateLimiter.isThrottled())) {
135-
// Keep event related span, because it could be ignored in case of
136-
// reduced datadog sampling rate.
137-
traceSeg.setTagTop(DDTags.MANUAL_KEEP, true);
138-
traceSeg.setTagTop("appsec.event", true);
139-
traceSeg.setTagTop("network.client.ip", ctx.getPeerAddress());
140-
141-
Map<String, List<String>> requestHeaders = ctx.getRequestHeaders();
142-
Map<String, List<String>> responseHeaders = ctx.getResponseHeaders();
143-
// Reflect client_ip as actor.ip for backward compatibility
144-
Object clientIp = spanInfo.getTags().get(Tags.HTTP_CLIENT_IP);
145-
if (clientIp != null) {
146-
traceSeg.setTagTop("actor.ip", clientIp);
147-
}
148-
149-
// Report AppSec events via "_dd.appsec.json" tag
150-
AppSecEventWrapper wrapper = new AppSecEventWrapper(collectedEvents);
151-
traceSeg.setDataTop("appsec", wrapper);
152-
153-
// Report collected request and response headers based on allow list
154-
if (requestHeaders != null) {
155-
requestHeaders.forEach(
156-
(name, value) -> {
157-
if (AppSecRequestContext.HEADERS_ALLOW_LIST.contains(name)) {
158-
String v = Strings.join(",", value);
159-
if (!v.isEmpty()) {
160-
traceSeg.setTagTop("http.request.headers." + name, v);
161-
}
162-
}
163-
});
164-
}
165-
if (responseHeaders != null) {
166-
responseHeaders.forEach(
167-
(name, value) -> {
168-
if (AppSecRequestContext.HEADERS_ALLOW_LIST.contains(name)) {
169-
String v = String.join(",", value);
170-
if (!v.isEmpty()) {
171-
traceSeg.setTagTop("http.response.headers." + name, v);
172-
}
173-
}
174-
});
134+
if (rateLimiter == null || !rateLimiter.isThrottled()) {
135+
// If detected any events - mark span at appsec.event
136+
if (!collectedEvents.isEmpty()) {
137+
// Keep event related span, because it could be ignored in case of
138+
// reduced datadog sampling rate.
139+
traceSeg.setTagTop(DDTags.MANUAL_KEEP, true);
140+
traceSeg.setTagTop("appsec.event", true);
141+
traceSeg.setTagTop("network.client.ip", ctx.getPeerAddress());
142+
143+
// Reflect client_ip as actor.ip for backward compatibility
144+
Object clientIp = spanInfo.getTags().get(Tags.HTTP_CLIENT_IP);
145+
if (clientIp != null) {
146+
traceSeg.setTagTop("actor.ip", clientIp);
147+
}
148+
149+
// Report AppSec events via "_dd.appsec.json" tag
150+
AppSecEventWrapper wrapper = new AppSecEventWrapper(collectedEvents);
151+
traceSeg.setDataTop("appsec", wrapper);
152+
153+
// Report collected request and response headers based on allow list
154+
writeRequestHeaders(traceSeg, HEADERS_ALLOW_LIST, ctx.getRequestHeaders());
155+
writeResponseHeaders(traceSeg, HEADERS_ALLOW_LIST, ctx.getResponseHeaders());
156+
} else {
157+
// Report minimum set of collected request headers
158+
writeRequestHeaders(
159+
traceSeg, DEFAULT_REQUEST_HEADERS_ALLOW_LIST, ctx.getRequestHeaders());
175160
}
176161
}
177-
178162
// If extracted any Api Schemas - commit them
179163
if (!ctx.commitApiSchemas(traceSeg)) {
180164
log.debug("Unable to commit, api security schemas and will be skipped");
@@ -425,6 +409,38 @@ public void stop() {
425409
subscriptionService.reset();
426410
}
427411

412+
private static void writeRequestHeaders(
413+
final TraceSegment traceSeg,
414+
final Set<String> allowed,
415+
final Map<String, List<String>> headers) {
416+
writeHeaders(traceSeg, "http.request.headers.", allowed, headers);
417+
}
418+
419+
private static void writeResponseHeaders(
420+
final TraceSegment traceSeg,
421+
final Set<String> allowed,
422+
final Map<String, List<String>> headers) {
423+
writeHeaders(traceSeg, "http.response.headers.", allowed, headers);
424+
}
425+
426+
private static void writeHeaders(
427+
final TraceSegment traceSeg,
428+
final String prefix,
429+
final Set<String> allowed,
430+
final Map<String, List<String>> headers) {
431+
if (headers != null) {
432+
headers.forEach(
433+
(name, value) -> {
434+
if (allowed.contains(name)) {
435+
String v = String.join(",", value);
436+
if (!v.isEmpty()) {
437+
traceSeg.setTagTop(prefix + name, v);
438+
}
439+
}
440+
});
441+
}
442+
}
443+
428444
private static class RequestContextSupplier implements Flow<AppSecRequestContext> {
429445
private static final Flow<AppSecRequestContext> EMPTY = new RequestContextSupplier(null);
430446

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,4 +763,29 @@ class GatewayBridgeSpecification extends DDSpecification {
763763
flowReqEnd == NoopFlow.INSTANCE
764764
0 * _
765765
}
766+
767+
void 'default request headers are always set when appsec is enabled'() {
768+
final mockAppSecCtx = Mock(AppSecRequestContext)
769+
mockAppSecCtx.requestHeaders >> [
770+
'accept': ['text/plain'],
771+
'content-type': ['application/json'],
772+
'host': ['localhost'],
773+
'user-agent': ['mozilla']
774+
]
775+
final mockCtx = Stub(RequestContext) {
776+
getData(RequestContextSlot.APPSEC) >> mockAppSecCtx
777+
getTraceSegment() >> traceSegment
778+
}
779+
final spanInfo = Mock(AgentSpan)
780+
781+
when:
782+
requestEndedCB.apply(mockCtx, spanInfo)
783+
784+
then:
785+
1 * mockAppSecCtx.transferCollectedEvents() >> []
786+
1 * traceSegment.setTagTop('http.request.headers.accept', 'text/plain')
787+
1 * traceSegment.setTagTop('http.request.headers.content-type', 'application/json')
788+
1 * traceSegment.setTagTop('http.request.headers.user-agent', 'mozilla')
789+
0 * traceSegment.setTagTop('http.request.headers.host', _)
790+
}
766791
}

0 commit comments

Comments
 (0)