Skip to content

Commit 8233fc5

Browse files
authored
Expand SSRF support in IAST to java.net.http.HttpClient (#7877)
1 parent a177616 commit 8233fc5

File tree

10 files changed

+222
-7
lines changed

10 files changed

+222
-7
lines changed

dd-java-agent/instrumentation/java-http-client/src/main/java11/datadog/trace/instrumentation/httpclient/JavaNetClientDecorator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ protected URI url(HttpRequest httpRequest) throws URISyntaxException {
3636
return httpRequest.uri();
3737
}
3838

39+
@Override
40+
protected Object sourceUrl(final HttpRequest request) {
41+
return request.uri();
42+
}
43+
3944
@Override
4045
protected int status(HttpResponse<?> httpResponse) {
4146
return httpResponse.statusCode();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
plugins {
2+
id 'idea'
3+
id 'java-test-fixtures'
4+
}
5+
6+
7+
apply from: "$rootDir/gradle/java.gradle"
8+
9+
description = 'iast-smoke-tests-utils-java-11'
10+
11+
idea {
12+
module {
13+
jdkName = '11'
14+
}
15+
}
16+
17+
dependencies {
18+
api project(':dd-smoke-tests')
19+
compileOnly group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.2.0.RELEASE'
20+
21+
testFixturesImplementation testFixtures(project(":dd-smoke-tests:iast-util"))
22+
}
23+
24+
project.tasks.withType(AbstractCompile).configureEach {
25+
setJavaVersion(it, 11)
26+
sourceCompatibility = JavaVersion.VERSION_11
27+
targetCompatibility = JavaVersion.VERSION_11
28+
if (it instanceof JavaCompile) {
29+
it.options.release.set(11)
30+
}
31+
}
32+
33+
forbiddenApisMain {
34+
failOnMissingClasses = false
35+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package datadog.smoketest.springboot.controller;
2+
3+
import java.net.URI;
4+
import java.net.http.HttpClient;
5+
import java.net.http.HttpRequest;
6+
import java.net.http.HttpResponse;
7+
import org.springframework.web.bind.annotation.PostMapping;
8+
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RequestParam;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
@RestController
13+
@RequestMapping("/ssrf")
14+
public class SsrfController {
15+
16+
@PostMapping("/java-net")
17+
public String javaNet(
18+
@RequestParam(value = "url", required = false) final String url,
19+
@RequestParam(value = "async", required = false) final boolean async,
20+
@RequestParam(value = "promise", required = false) final boolean promise) {
21+
HttpClient httpClient = HttpClient.newBuilder().build();
22+
try {
23+
HttpRequest httpRequest = HttpRequest.newBuilder().uri(new URI(url)).build();
24+
if (async) {
25+
if (promise) {
26+
httpClient.sendAsync(
27+
httpRequest,
28+
HttpResponse.BodyHandlers.ofString(),
29+
(initiatingRequest, pushPromiseRequest, acceptor) -> {});
30+
} else {
31+
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString());
32+
}
33+
} else {
34+
httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
35+
}
36+
} catch (Exception e) {
37+
}
38+
return "ok";
39+
}
40+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package datadog.smoketest
2+
3+
import okhttp3.FormBody
4+
import okhttp3.Request
5+
6+
import static datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED
7+
import static datadog.trace.api.config.IastConfig.IAST_DETECTION_MODE
8+
import static datadog.trace.api.config.IastConfig.IAST_ENABLED
9+
10+
abstract class AbstractIast11SpringBootTest extends AbstractIastServerSmokeTest {
11+
12+
@Override
13+
ProcessBuilder createProcessBuilder() {
14+
String springBootShadowJar = System.getProperty('datadog.smoketest.springboot.shadowJar.path')
15+
16+
List<String> command = []
17+
command.add(javaPath())
18+
command.addAll(defaultJavaProperties)
19+
command.addAll(iastJvmOpts())
20+
command.addAll((String[]) ['-jar', springBootShadowJar, "--server.port=${httpPort}"])
21+
ProcessBuilder processBuilder = new ProcessBuilder(command)
22+
processBuilder.directory(new File(buildDirectory))
23+
// Spring will print all environment variables to the log, which may pollute it and affect log assertions.
24+
processBuilder.environment().clear()
25+
return processBuilder
26+
}
27+
28+
protected List<String> iastJvmOpts() {
29+
return [
30+
withSystemProperty(IAST_ENABLED, true),
31+
withSystemProperty(IAST_DETECTION_MODE, 'FULL'),
32+
withSystemProperty(IAST_DEBUG_ENABLED, true),
33+
]
34+
}
35+
36+
void 'ssrf is present (#path)'() {
37+
setup:
38+
final url = "http://localhost:${httpPort}/ssrf/${path}"
39+
final body = new FormBody.Builder()
40+
.add(parameter, value)
41+
.add("async", async)
42+
.add("promise", promise).build()
43+
final request = new Request.Builder().url(url).post(body).build()
44+
45+
when:
46+
client.newCall(request).execute()
47+
48+
then:
49+
hasVulnerability { vul ->
50+
if (vul.type != 'SSRF') {
51+
return false
52+
}
53+
final parts = vul.evidence.valueParts
54+
if (parameter == 'url') {
55+
return parts.size() == 1
56+
&& parts[0].value == value && parts[0].source.origin == 'http.request.parameter' && parts[0].source.name == parameter
57+
} else {
58+
throw new IllegalArgumentException("Parameter $parameter not supported")
59+
}
60+
}
61+
62+
where:
63+
path | parameter | value | async | promise
64+
"java-net" | "url" | "https://dd.datad0g.com/" | "false" | "false"
65+
"java-net" | "url" | "https://dd.datad0g.com/" | "true" | "false"
66+
"java-net" | "url" | "https://dd.datad0g.com/" | "true" | "true"
67+
}
68+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ abstract class AbstractIastServerSmokeTest extends AbstractServerSmokeTest {
119119
}
120120
final vulnerabilities = parseVulnerabilities(json)
121121
found.addAll(vulnerabilities)
122+
return vulnerabilities.find(matcher) != null
122123
}
123124
} catch (SpockTimeoutError toe) {
124125
throw new AssertionError("No matching vulnerability found. Vulnerabilities found: ${new JsonBuilder(found).toPrettyString()}")

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -753,12 +753,12 @@ abstract class AbstractIastSpringBootTest extends AbstractIastServerSmokeTest {
753753
}
754754

755755
where:
756-
path | parameter | value | method | protocolSecure
757-
"apache-httpclient4" | "url" | "https://dd.datad0g.com/" | "apacheHttpClient4" | false
758-
"apache-httpclient4" | "host" | "dd.datad0g.com" | "apacheHttpClient4" | false
759-
"commons-httpclient2" | "url" | "https://dd.datad0g.com/" | "commonsHttpClient2" | false
760-
"okHttp2" | "url" | "https://dd.datad0g.com/" | "okHttp2" | false
761-
"okHttp3" | "url" | "https://dd.datad0g.com/" | "okHttp3" | false
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
762762
}
763763

764764
void 'test iast metrics stored in spans'() {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '2.7.15'
4+
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
5+
id 'java-test-fixtures'
6+
}
7+
8+
ext {
9+
minJavaVersionForTests = JavaVersion.VERSION_11
10+
}
11+
12+
apply from: "$rootDir/gradle/java.gradle"
13+
description = 'SpringBoot Java 11 Smoke Tests.'
14+
15+
repositories {
16+
mavenCentral()
17+
}
18+
19+
dependencies {
20+
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.2.0.RELEASE'
21+
22+
testImplementation project(':dd-smoke-tests')
23+
testImplementation testFixtures(project(":dd-smoke-tests:iast-util:iast-util-11"))
24+
testImplementation testFixtures(project(':dd-smoke-tests:iast-util'))
25+
26+
implementation project(':dd-smoke-tests:iast-util:iast-util-11')
27+
}
28+
29+
project.tasks.withType(AbstractCompile).configureEach {
30+
setJavaVersion(it, 11)
31+
sourceCompatibility = JavaVersion.VERSION_11
32+
targetCompatibility = JavaVersion.VERSION_11
33+
if (it instanceof JavaCompile) {
34+
it.options.release.set(11)
35+
}
36+
}
37+
38+
forbiddenApisMain {
39+
failOnMissingClasses = false
40+
}
41+
42+
tasks.withType(Test).configureEach {
43+
dependsOn "bootJar"
44+
jvmArgs "-Ddatadog.smoketest.springboot.shadowJar.path=${tasks.bootJar.archiveFile.get()}"
45+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package datadog.smoketest.springboot;
2+
3+
import java.lang.management.ManagementFactory;
4+
import org.springframework.boot.SpringApplication;
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
7+
@SpringBootApplication
8+
public class SpringbootApplication {
9+
10+
public static void main(final String[] args) {
11+
SpringApplication.run(SpringbootApplication.class, args);
12+
System.out.println("Started in " + ManagementFactory.getRuntimeMXBean().getUptime() + "ms");
13+
}
14+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package datadog.smoketest.springboot
2+
3+
import datadog.smoketest.AbstractIast11SpringBootTest
4+
5+
class IastSpringBootSmokeTest extends AbstractIast11SpringBootTest {
6+
}

settings.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ include ':dd-smoke-tests:spring-security'
139139
include ':dd-smoke-tests:springboot'
140140
include ':dd-smoke-tests:springboot-freemarker'
141141
include ':dd-smoke-tests:springboot-grpc'
142+
include ':dd-smoke-tests:springboot-java-11'
142143
include ':dd-smoke-tests:springboot-jetty-jsp'
143144
include ':dd-smoke-tests:springboot-mongo'
144145
include ':dd-smoke-tests:springboot-openliberty-20'
@@ -162,6 +163,7 @@ include ':dd-smoke-tests:debugger-integration-tests'
162163
include ':dd-smoke-tests:datastreams:kafkaschemaregistry'
163164
include ':dd-smoke-tests:iast-propagation'
164165
include ':dd-smoke-tests:iast-util'
166+
include ':dd-smoke-tests:iast-util:iast-util-11'
165167
// TODO this fails too often with a jgit failure, so disable until fixed
166168
//include ':dd-smoke-tests:debugger-integration-tests:latest-jdk-app'
167169

@@ -500,4 +502,3 @@ include ':dd-java-agent:benchmark'
500502
include ':dd-java-agent:benchmark-integration'
501503
include ':dd-java-agent:benchmark-integration:jetty-perftest'
502504
include ':dd-java-agent:benchmark-integration:play-perftest'
503-

0 commit comments

Comments
 (0)