Skip to content

Commit 9ac8ab1

Browse files
authored
Expand SSRF support in IAST to apache-httpclient-5 and apache-httpasyncclient-4 (#7920)
1 parent 4df0a01 commit 9ac8ab1

File tree

17 files changed

+330
-11
lines changed

17 files changed

+330
-11
lines changed

dd-java-agent/instrumentation/apache-httpasyncclient-4/src/main/java/datadog/trace/instrumentation/apachehttpasyncclient/ApacheHttpAsyncClientDecorator.java

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ protected URI url(final HttpUriRequest request) throws URISyntaxException {
4141
return request.getURI();
4242
}
4343

44+
@Override
45+
protected URI sourceUrl(final HttpUriRequest request) {
46+
return request.getURI();
47+
}
48+
4449
@Override
4550
protected int status(final HttpContext context) {
4651
final Object responseObject = context.getAttribute(HttpCoreContext.HTTP_RESPONSE);

dd-java-agent/instrumentation/apache-httpclient-5/src/main/java/datadog/trace/instrumentation/apachehttpclient5/ApacheHttpClientDecorator.java

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ protected URI url(final HttpRequest request) throws URISyntaxException {
4242
return request.getUri();
4343
}
4444

45+
@Override
46+
protected HttpRequest sourceUrl(final HttpRequest request) {
47+
return request;
48+
}
49+
4550
@Override
4651
protected int status(final HttpResponse httpResponse) {
4752
return httpResponse.getCode();

dd-java-agent/instrumentation/apache-httpclient-5/src/main/java/datadog/trace/instrumentation/apachehttpclient5/HostAndRequestAsHttpUriRequest.java

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package datadog.trace.instrumentation.apachehttpclient5;
22

3+
import datadog.trace.api.iast.util.PropagationUtils;
34
import java.net.URI;
45
import java.net.URISyntaxException;
56
import org.apache.hc.core5.http.Header;
@@ -15,6 +16,9 @@ public class HostAndRequestAsHttpUriRequest extends BasicClassicHttpRequest {
1516
public HostAndRequestAsHttpUriRequest(final HttpHost httpHost, final HttpRequest httpRequest) {
1617
super(httpRequest.getMethod(), httpHost, httpRequest.getPath());
1718
actualRequest = httpRequest;
19+
// Propagate in case the host or request is tainted
20+
PropagationUtils.taintObjectIfTainted(this, httpHost);
21+
PropagationUtils.taintObjectIfTainted(this, httpRequest);
1822
}
1923

2024
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package datadog.trace.instrumentation.apachehttpclient5;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
4+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
5+
6+
import com.google.auto.service.AutoService;
7+
import datadog.trace.agent.tooling.Instrumenter;
8+
import datadog.trace.agent.tooling.InstrumenterModule;
9+
import datadog.trace.agent.tooling.bytebuddy.iast.TaintableVisitor;
10+
import datadog.trace.api.iast.InstrumentationBridge;
11+
import datadog.trace.api.iast.Propagation;
12+
import datadog.trace.api.iast.propagation.PropagationModule;
13+
import java.net.URI;
14+
import net.bytebuddy.asm.Advice;
15+
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
16+
17+
@AutoService(InstrumenterModule.class)
18+
public class IastHttpUriRequestBaseInstrumentation extends InstrumenterModule.Iast
19+
implements Instrumenter.ForSingleType, Instrumenter.HasTypeAdvice {
20+
21+
public IastHttpUriRequestBaseInstrumentation() {
22+
super("apache-httpclient", "httpclient5");
23+
}
24+
25+
@Override
26+
public String instrumentedType() {
27+
return "org.apache.hc.client5.http.classic.methods.HttpUriRequestBase";
28+
}
29+
30+
@Override
31+
public void typeAdvice(TypeTransformer transformer) {
32+
transformer.applyAdvice(new TaintableVisitor(instrumentedType()));
33+
}
34+
35+
@Override
36+
public void methodAdvice(MethodTransformer transformer) {
37+
transformer.applyAdvice(
38+
isConstructor().and(takesArguments(String.class, URI.class)),
39+
IastHttpUriRequestBaseInstrumentation.class.getName() + "$CtorAdvice");
40+
}
41+
42+
public static class CtorAdvice {
43+
@Advice.OnMethodExit()
44+
@Propagation
45+
public static void afterCtor(
46+
@Advice.This final HttpUriRequestBase self, @Advice.Argument(1) final URI uri) {
47+
final PropagationModule module = InstrumentationBridge.PROPAGATION;
48+
if (module != null) {
49+
module.taintObjectIfTainted(self, uri);
50+
}
51+
}
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import datadog.trace.agent.test.AgentTestRunner
2+
import datadog.trace.api.iast.InstrumentationBridge
3+
import datadog.trace.api.iast.propagation.PropagationModule
4+
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase
5+
6+
class IastHttpUriRequestBaseInstrumentationTest extends AgentTestRunner {
7+
8+
@Override
9+
protected void configurePreAgent() {
10+
injectSysConfig('dd.iast.enabled', 'true')
11+
}
12+
13+
void 'test constructor'() {
14+
given:
15+
final module = Mock(PropagationModule)
16+
InstrumentationBridge.registerIastModule(module)
17+
18+
when:
19+
HttpUriRequestBase.newInstance(method, new URI(uri))
20+
21+
then:
22+
1 * module.taintObjectIfTainted(_ as HttpUriRequestBase, _ as URI)
23+
0 * _
24+
25+
where:
26+
method | uri
27+
"GET" | 'http://localhost.com'
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
muzzle {
2+
pass {
3+
group = "org.apache.httpcomponents.core5"
4+
module = "httpcore5"
5+
versions = "[5.0,)"
6+
assertInverse = true
7+
}
8+
}
9+
10+
apply from: "$rootDir/gradle/java.gradle"
11+
12+
addTestSuiteForDir('latestDepTest', 'test')
13+
14+
dependencies {
15+
compileOnly group: 'org.apache.httpcomponents.core5', name: 'httpcore5', version: '5.0'
16+
17+
testImplementation group: 'org.apache.httpcomponents.core5', name: 'httpcore5', version: '5.0'
18+
19+
latestDepTestImplementation group: 'org.apache.httpcomponents.core5', name: 'httpcore5', version: '+'
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package datadog.trace.instrumentation.apachehttpcore5;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
4+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
5+
6+
import com.google.auto.service.AutoService;
7+
import datadog.trace.agent.tooling.Instrumenter;
8+
import datadog.trace.agent.tooling.InstrumenterModule;
9+
import datadog.trace.api.iast.InstrumentationBridge;
10+
import datadog.trace.api.iast.Propagation;
11+
import datadog.trace.api.iast.propagation.PropagationModule;
12+
import java.net.InetAddress;
13+
import net.bytebuddy.asm.Advice;
14+
15+
@AutoService(InstrumenterModule.class)
16+
public class IastHttpHostInstrumentation extends InstrumenterModule.Iast
17+
implements Instrumenter.ForSingleType {
18+
19+
public IastHttpHostInstrumentation() {
20+
super("httpcore-5", "apache-httpcore-5", "apache-http-core-5");
21+
}
22+
23+
@Override
24+
public String instrumentedType() {
25+
return "org.apache.hc.core5.http.HttpHost";
26+
}
27+
28+
@Override
29+
public void methodAdvice(MethodTransformer transformer) {
30+
transformer.applyAdvice(
31+
isConstructor()
32+
.and(takesArguments(String.class, InetAddress.class, String.class, int.class)),
33+
IastHttpHostInstrumentation.class.getName() + "$CtorAdvice");
34+
}
35+
36+
public static class CtorAdvice {
37+
@Advice.OnMethodExit(suppress = Throwable.class)
38+
@Propagation
39+
public static void afterCtor(
40+
@Advice.This final Object self, @Advice.Argument(2) final String host) {
41+
final PropagationModule module = InstrumentationBridge.PROPAGATION;
42+
if (module != null) {
43+
module.taintObjectIfTainted(self, host);
44+
}
45+
}
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package datadog.trace.instrumentation.apachehttpcore5
2+
3+
import datadog.trace.agent.test.AgentTestRunner
4+
import datadog.trace.api.iast.InstrumentationBridge
5+
import datadog.trace.api.iast.propagation.PropagationModule
6+
import org.apache.hc.core5.http.HttpHost
7+
8+
class IastHttpHostInstrumentationTest extends AgentTestRunner {
9+
10+
@Override
11+
protected void configurePreAgent() {
12+
injectSysConfig('dd.iast.enabled', 'true')
13+
}
14+
15+
void 'test constructor'(){
16+
given:
17+
final module = Mock(PropagationModule)
18+
InstrumentationBridge.registerIastModule(module)
19+
20+
when:
21+
HttpHost.newInstance(*args)
22+
23+
then:
24+
1 * module.taintObjectIfTainted( _ as HttpHost, 'localhost')
25+
26+
where:
27+
args | _
28+
['localhost'] | _
29+
['localhost', 8080] | _
30+
}
31+
}

dd-java-agent/instrumentation/iast-instrumenter/src/main/resources/datadog/trace/instrumentation/iastinstrumenter/iast_exclusion.trie

+1
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@
198198
1 org.apache.*
199199
#apache httpClient needs URI propagation
200200
0 org.apache.http.client.methods.*
201+
0 org.apache.hc.client5.http.classic.methods.*
201202
# apache compiled jsps
202203
0 org.apache.jsp.*
203204
1 org.apiguardian.*

dd-smoke-tests/iast-util/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ dependencies {
1717
compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'
1818
compileOnly group: 'com.squareup.okhttp', name: 'okhttp', version: '2.2.0'
1919
compileOnly group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.0.0'
20+
compileOnly group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0'
21+
compileOnly group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.0'
2022
}

dd-smoke-tests/iast-util/src/main/java/datadog/smoketest/springboot/controller/SsrfController.java

+62
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@
77
import org.apache.commons.httpclient.HttpClient;
88
import org.apache.commons.httpclient.HttpMethod;
99
import org.apache.commons.httpclient.methods.GetMethod;
10+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
11+
import org.apache.hc.client5.http.impl.classic.HttpClients;
1012
import org.apache.http.HttpHost;
1113
import org.apache.http.client.methods.HttpGet;
1214
import org.apache.http.impl.client.DefaultHttpClient;
15+
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
16+
import org.apache.http.impl.nio.client.HttpAsyncClients;
1317
import org.apache.http.message.BasicHttpRequest;
18+
import org.apache.http.nio.client.methods.HttpAsyncMethods;
19+
import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
1420
import org.springframework.web.bind.annotation.PostMapping;
1521
import org.springframework.web.bind.annotation.RequestMapping;
1622
import org.springframework.web.bind.annotation.RequestParam;
@@ -89,4 +95,60 @@ public String okHttp3(@RequestParam(value = "url") final String url) {
8995
client.connectionPool().evictAll();
9096
return "ok";
9197
}
98+
99+
@PostMapping("/apache-httpclient5")
100+
public String apacheHttpClient5(
101+
@RequestParam(value = "url", required = false) final String url,
102+
@RequestParam(value = "urlHandler", required = false) final String urlHandler,
103+
@RequestParam(value = "host", required = false) final String host) {
104+
CloseableHttpClient client = HttpClients.createDefault();
105+
try {
106+
if (host != null) {
107+
final org.apache.hc.core5.http.HttpHost httpHost =
108+
new org.apache.hc.core5.http.HttpHost(host);
109+
final org.apache.hc.client5.http.classic.methods.HttpGet request =
110+
new org.apache.hc.client5.http.classic.methods.HttpGet("/");
111+
client.execute(httpHost, request);
112+
} else if (url != null) {
113+
final org.apache.hc.client5.http.classic.methods.HttpGet request =
114+
new org.apache.hc.client5.http.classic.methods.HttpGet(url);
115+
client.execute(request);
116+
} else if (urlHandler != null) {
117+
final org.apache.hc.client5.http.classic.methods.HttpGet request =
118+
new org.apache.hc.client5.http.classic.methods.HttpGet(urlHandler);
119+
client.execute(request, response -> null);
120+
}
121+
client.close();
122+
} catch (Exception e) {
123+
}
124+
return "ok";
125+
}
126+
127+
@PostMapping("/apache-httpasyncclient")
128+
public String apacheHttpAsyncClient(
129+
@RequestParam(value = "url", required = false) final String url,
130+
@RequestParam(value = "host", required = false) final String host,
131+
@RequestParam(value = "urlProducer", required = false) final String urlProducer) {
132+
final CloseableHttpAsyncClient client = HttpAsyncClients.createDefault();
133+
client.start();
134+
try {
135+
if (host != null) {
136+
final HttpHost httpHost = new HttpHost(host);
137+
client.execute(httpHost, new HttpGet("/"), null);
138+
} else if (url != null) {
139+
final HttpGet request = new HttpGet(url);
140+
client.execute(request, null);
141+
} else if (urlProducer != null) {
142+
final HttpAsyncRequestProducer producer = HttpAsyncMethods.create(new HttpGet(urlProducer));
143+
client.execute(producer, null, null);
144+
}
145+
} catch (Exception e) {
146+
} finally {
147+
try {
148+
client.close();
149+
} catch (Exception e) {
150+
}
151+
}
152+
return "ok";
153+
}
92154
}

dd-smoke-tests/iast-util/src/testFixtures/groovy/datadog/smoketest/AbstractIastSpringBootTest.groovy

+18-10
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ abstract class AbstractIastSpringBootTest extends AbstractIastServerSmokeTest {
724724
'host' | 'dd.datad0g.com'
725725
}
726726

727-
void 'ssrf is present (#path)'() {
727+
void 'ssrf is present (#path) (#parameter)'() {
728728
setup:
729729
final url = "http://localhost:${httpPort}/ssrf/${path}"
730730
final body = new FormBody.Builder().add(parameter, value).build()
@@ -744,21 +744,29 @@ abstract class AbstractIastSpringBootTest extends AbstractIastServerSmokeTest {
744744
&& parts[0].value == value && parts[0].source.origin == 'http.request.parameter' && parts[0].source.name == parameter
745745
} else if (parameter == 'host') {
746746
String protocol = protocolSecure ? 'https://' : 'http://'
747-
return parts.size() == 2
748-
&& parts[0].value == protocol + value && parts[0].source.origin == 'http.request.parameter' && parts[0].source.name == parameter
749-
&& parts[1].value == '/' && parts[1].source == null
747+
String finalValue = protocol + value + (endSlash ? '/' : '')
748+
return parts[0].value.endsWith(finalValue) && parts[0].source.origin == 'http.request.parameter' && parts[0].source.name == parameter
749+
} else if (parameter == 'urlProducer' || parameter == 'urlHandler') {
750+
return parts.size() == 1
751+
&& parts[0].value.endsWith(value) && parts[0].source.origin == 'http.request.parameter' && parts[0].source.name == parameter
750752
} else {
751753
throw new IllegalArgumentException("Parameter $parameter not supported")
752754
}
753755
}
754756

755757
where:
756-
path | parameter | value | protocolSecure
757-
"apache-httpclient4" | "url" | "https://dd.datad0g.com/" | true
758-
"apache-httpclient4" | "host" | "dd.datad0g.com" | false
759-
"commons-httpclient2" | "url" | "https://dd.datad0g.com/" | true
760-
"okHttp2" | "url" | "https://dd.datad0g.com/" | true
761-
"okHttp3" | "url" | "https://dd.datad0g.com/" | true
758+
path | parameter | value | protocolSecure | endSlash
759+
"apache-httpclient4" | "url" | "https://dd.datad0g.com/" | true | true
760+
"apache-httpclient4" | "host" | "dd.datad0g.com" | false | false
761+
"apache-httpasyncclient" | "url" | "https://dd.datad0g.com/" | true | true
762+
"apache-httpasyncclient" | "urlProducer" | "https://dd.datad0g.com/" | true | true
763+
"apache-httpasyncclient" | "host" | "dd.datad0g.com" | false | false
764+
"apache-httpclient5" | "url" | "https://dd.datad0g.com/" | true | true
765+
"apache-httpclient5" | "urlHandler" | "https://dd.datad0g.com/" | true | true
766+
"apache-httpclient5" | "host" | "dd.datad0g.com" | false | true
767+
"commons-httpclient2" | "url" | "https://dd.datad0g.com/" | true | true
768+
"okHttp2" | "url" | "https://dd.datad0g.com/" | true | true
769+
"okHttp3" | "url" | "https://dd.datad0g.com/" | true | true
762770
}
763771

764772
void 'test iast metrics stored in spans'() {

dd-smoke-tests/spring-boot-2.6-webmvc/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ dependencies {
4242
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'
4343
implementation group: 'com.squareup.okhttp', name: 'okhttp', version: '2.2.0'
4444
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.0.0'
45+
implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0'
46+
implementation group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.0'
4547

4648
testImplementation project(':dd-smoke-tests')
4749
implementation project(':dd-smoke-tests:iast-util')

dd-smoke-tests/springboot/build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ dependencies {
3333
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'
3434
implementation group: 'com.squareup.okhttp', name: 'okhttp', version: '2.2.0'
3535
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.0.0'
36-
36+
implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0'
37+
implementation group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.0'
3738

3839
testImplementation project(':dd-smoke-tests')
3940
testImplementation(testFixtures(project(":dd-smoke-tests:iast-util")))

0 commit comments

Comments
 (0)