Skip to content

Commit e10c32e

Browse files
Initial implementation of http client request analysis for OkHttp2
1 parent abb87ab commit e10c32e

File tree

36 files changed

+2024
-86
lines changed

36 files changed

+2024
-86
lines changed

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecorator.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import datadog.trace.api.DDTags;
1111
import datadog.trace.api.InstrumenterConfig;
1212
import datadog.trace.api.ProductActivation;
13+
import datadog.trace.api.appsec.HttpClientRequest;
1314
import datadog.trace.api.gateway.BlockResponseFunction;
1415
import datadog.trace.api.gateway.Flow;
1516
import datadog.trace.api.gateway.RequestContext;
@@ -99,7 +100,7 @@ public AgentSpan onRequest(final AgentSpan span, final REQUEST request) {
99100
HTTP_RESOURCE_DECORATOR.withClientPath(span, method, url.getPath());
100101
}
101102
// SSRF exploit prevention check
102-
onNetworkConnection(url.toString());
103+
onHttpClientRequest(span, url.toString());
103104
} else if (shouldSetResourceName()) {
104105
span.setResourceName(DEFAULT_RESOURCE_NAME);
105106
}
@@ -178,24 +179,20 @@ public long getResponseContentLength(final RESPONSE response) {
178179
return 0;
179180
}
180181

181-
private void onNetworkConnection(final String networkConnection) {
182+
protected void onHttpClientRequest(final AgentSpan span, final String url) {
182183
if (!APPSEC_RASP_ENABLED) {
183184
return;
184185
}
185-
if (networkConnection == null) {
186+
if (url == null) {
186187
return;
187188
}
188-
final BiFunction<RequestContext, String, Flow<Void>> networkConnectionCallback =
189+
final long requestId = span.getSpanId();
190+
final BiFunction<RequestContext, HttpClientRequest, Flow<Void>> requestCb =
189191
AgentTracer.get()
190192
.getCallbackProvider(RequestContextSlot.APPSEC)
191-
.getCallback(EVENTS.networkConnection());
193+
.getCallback(EVENTS.httpClientRequest());
192194

193-
if (networkConnectionCallback == null) {
194-
return;
195-
}
196-
197-
final AgentSpan span = AgentTracer.get().activeSpan();
198-
if (span == null) {
195+
if (requestCb == null) {
199196
return;
200197
}
201198

@@ -204,7 +201,7 @@ private void onNetworkConnection(final String networkConnection) {
204201
return;
205202
}
206203

207-
Flow<Void> flow = networkConnectionCallback.apply(ctx, networkConnection);
204+
Flow<Void> flow = requestCb.apply(ctx, new HttpClientRequest(requestId, url));
208205
Flow.Action action = flow.getAction();
209206
if (action instanceof Flow.Action.RequestBlockingAction) {
210207
BlockResponseFunction brf = ctx.getBlockResponseFunction();

dd-java-agent/agent-bootstrap/src/test/groovy/datadog/trace/bootstrap/instrumentation/decorator/HttpClientDecoratorTest.groovy

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package datadog.trace.bootstrap.instrumentation.decorator
22

33
import datadog.trace.api.DDTags
4+
import datadog.trace.api.appsec.HttpClientRequest
45
import datadog.trace.api.config.AppSecConfig
56
import datadog.trace.api.gateway.CallbackProvider
67
import static datadog.trace.api.gateway.Events.EVENTS
@@ -249,8 +250,8 @@ class HttpClientDecoratorTest extends ClientDecoratorTest {
249250
decorator.onRequest(span2, req)
250251

251252
then:
252-
1 * callbackProvider.getCallback(EVENTS.networkConnection()) >> listener
253-
1 * listener.apply(reqCtx, _ as String)
253+
1 * callbackProvider.getCallback(EVENTS.httpClientRequest()) >> listener
254+
1 * listener.apply(reqCtx, _ as HttpClientRequest)
254255
}
255256

256257
@Override

dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.datadog.appsec;
22

3+
import com.datadog.appsec.api.security.ApiSecurityDownstreamSampler;
34
import com.datadog.appsec.api.security.ApiSecuritySampler;
45
import com.datadog.appsec.api.security.ApiSecuritySamplerImpl;
56
import com.datadog.appsec.api.security.AppSecSpanPostProcessor;
@@ -81,11 +82,14 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s
8182
}
8283
sco.createRemaining(config);
8384

85+
final double maxDownstreamRequestsRate =
86+
config.getApiSecurityDownstreamRequestAnalysisSampleRate();
8487
GatewayBridge gatewayBridge =
8588
new GatewayBridge(
8689
gw,
8790
REPLACEABLE_EVENT_PRODUCER,
8891
() -> API_SECURITY_SAMPLER,
92+
ApiSecurityDownstreamSampler.build(maxDownstreamRequestsRate),
8993
APP_SEC_CONFIG_SERVICE.getTraceSegmentPostProcessors());
9094

9195
loadModules(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.datadog.appsec.api.security;
2+
3+
import com.datadog.appsec.gateway.AppSecRequestContext;
4+
5+
public interface ApiSecurityDownstreamSampler {
6+
7+
boolean sampleHttpClientRequest(AppSecRequestContext ctx, long requestId);
8+
9+
boolean isSampled(AppSecRequestContext ctx, long requestId);
10+
11+
ApiSecurityDownstreamSampler INCLUDE_ALL =
12+
new ApiSecurityDownstreamSampler() {
13+
@Override
14+
public boolean sampleHttpClientRequest(AppSecRequestContext ctx, long requestId) {
15+
return true;
16+
}
17+
18+
@Override
19+
public boolean isSampled(AppSecRequestContext ctx, long requestId) {
20+
return true;
21+
}
22+
};
23+
24+
ApiSecurityDownstreamSampler INCLUDE_NONE =
25+
new ApiSecurityDownstreamSampler() {
26+
@Override
27+
public boolean sampleHttpClientRequest(AppSecRequestContext ctx, long requestId) {
28+
return false;
29+
}
30+
31+
@Override
32+
public boolean isSampled(AppSecRequestContext ctx, long requestId) {
33+
return false;
34+
}
35+
};
36+
37+
static ApiSecurityDownstreamSampler build(double rate) {
38+
return rate <= 0D
39+
? INCLUDE_NONE
40+
: (rate >= 1D ? INCLUDE_ALL : new ApiSecurityDownstreamSamplerImpl(rate));
41+
}
42+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.datadog.appsec.api.security;
2+
3+
import com.datadog.appsec.gateway.AppSecRequestContext;
4+
import java.util.concurrent.atomic.AtomicLong;
5+
6+
public class ApiSecurityDownstreamSamplerImpl implements ApiSecurityDownstreamSampler {
7+
8+
private static final long KNUTH_FACTOR = 1111111111111111111L;
9+
private static final double SAMPLING_MAX = Math.pow(2, 64) - 1;
10+
11+
private final AtomicLong globalRequestCount = new AtomicLong(0);
12+
private final double threshold;
13+
14+
public ApiSecurityDownstreamSamplerImpl(double rate) {
15+
threshold = samplingCutoff(rate);
16+
}
17+
18+
private static double samplingCutoff(double rate) {
19+
if (rate < 0.5) {
20+
return (long) (rate * SAMPLING_MAX) + Long.MIN_VALUE;
21+
}
22+
if (rate < 1.0) {
23+
return (long) ((rate * SAMPLING_MAX) + Long.MIN_VALUE);
24+
}
25+
return Long.MAX_VALUE;
26+
}
27+
28+
/**
29+
* First sample the request to ensure we randomize the request and then check if the current
30+
* server request has budget to analyze the downstream request.
31+
*/
32+
@Override
33+
public boolean sampleHttpClientRequest(final AppSecRequestContext ctx, final long requestId) {
34+
final long counter = updateRequestCount();
35+
if (counter * KNUTH_FACTOR + Long.MIN_VALUE > threshold) {
36+
return false;
37+
}
38+
return ctx.maybeSampleHttpClientRequest(requestId);
39+
}
40+
41+
@Override
42+
public boolean isSampled(final AppSecRequestContext ctx, final long requestId) {
43+
return ctx.isHttpClientRequestSampled(requestId);
44+
}
45+
46+
private long updateRequestCount() {
47+
return globalRequestCount.updateAndGet(current -> (current + 1) < 0 ? 0 : current + 1);
48+
}
49+
}

dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ public interface KnownAddresses {
118118
/** The URL of a network resource being requested (outgoing request) */
119119
Address<String> IO_NET_URL = new Address<>("server.io.net.url");
120120

121+
/** The headers of a network resource being requested (outgoing request) */
122+
Address<Map<String, List<String>>> IO_NET_REQUEST_HEADERS =
123+
new Address<>("server.io.net.request.headers");
124+
125+
/** The method of a network resource being requested (outgoing request) */
126+
Address<String> IO_NET_REQUEST_METHOD = new Address<>("server.io.net.request.method");
127+
128+
/** The body of a network resource being requested (outgoing request) */
129+
Address<Object> IO_NET_REQUEST_BODY = new Address<>("server.io.net.request.body");
130+
131+
/** The status of a network resource being requested (outgoing request) */
132+
Address<Integer> IO_NET_RESPONSE_STATUS = new Address<>("server.io.net.response.status");
133+
134+
/** The response headers of a network resource being requested (outgoing request) */
135+
Address<Map<String, List<String>>> IO_NET_RESPONSE_HEADERS =
136+
new Address<>("server.io.net.response.headers");
137+
138+
/** The response body of a network resource being requested (outgoing request) */
139+
Address<Object> IO_NET_RESPONSE_BODY = new Address<>("server.io.net.response.body");
140+
121141
/** The representation of opened file on the filesystem */
122142
Address<String> IO_FS_FILE = new Address<>("server.io.fs.file");
123143

@@ -206,6 +226,18 @@ static Address<?> forName(String name) {
206226
return SESSION_ID;
207227
case "server.io.net.url":
208228
return IO_NET_URL;
229+
case "server.io.net.request.headers":
230+
return IO_NET_REQUEST_HEADERS;
231+
case "server.io.net.request.method":
232+
return IO_NET_REQUEST_METHOD;
233+
case "server.io.net.request.body":
234+
return IO_NET_REQUEST_BODY;
235+
case "server.io.net.response.status":
236+
return IO_NET_RESPONSE_STATUS;
237+
case "server.io.net.response.headers":
238+
return IO_NET_RESPONSE_HEADERS;
239+
case "server.io.net.response.body":
240+
return IO_NET_RESPONSE_BODY;
209241
case "server.io.fs.file":
210242
return IO_FS_FILE;
211243
case "server.db.system":

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ public class AppSecRequestContext implements DataBundle, Closeable {
149149
private volatile Long apiSecurityEndpointHash;
150150
private volatile byte keepType = PrioritySampling.SAMPLER_KEEP;
151151

152+
private static final AtomicInteger httpClientRequestCount = new AtomicInteger(0);
153+
private static final Set<Long> httpClientRequests =
154+
Collections.newSetFromMap(new ConcurrentHashMap<>());
155+
152156
private static final AtomicIntegerFieldUpdater<AppSecRequestContext> WAF_TIMEOUTS_UPDATER =
153157
AtomicIntegerFieldUpdater.newUpdater(AppSecRequestContext.class, "wafTimeouts");
154158
private static final AtomicIntegerFieldUpdater<AppSecRequestContext> RASP_TIMEOUTS_UPDATER =
@@ -235,6 +239,28 @@ public void increaseRaspTimeouts() {
235239
RASP_TIMEOUTS_UPDATER.incrementAndGet(this);
236240
}
237241

242+
public boolean maybeSampleHttpClientRequest(final long id) {
243+
final boolean[] added = new boolean[1];
244+
httpClientRequestCount.updateAndGet(
245+
curr -> {
246+
if (curr < Config.get().getApiSecurityMaxDownstreamRequestBodyAnalysis()) {
247+
httpClientRequests.add(id);
248+
added[0] = true;
249+
return curr + 1;
250+
}
251+
return curr;
252+
});
253+
return added[0];
254+
}
255+
256+
public boolean isHttpClientRequestSampled(final long id) {
257+
return httpClientRequests.contains(id);
258+
}
259+
260+
public int getHttpClientRequestCount() {
261+
return httpClientRequestCount.get();
262+
}
263+
238264
public int getWafTimeouts() {
239265
return wafTimeouts;
240266
}

0 commit comments

Comments
 (0)