Skip to content

Commit

Permalink
Support SSRF in Apache HttpClient V4 (#6112)
Browse files Browse the repository at this point in the history
What Does This Do
Add new IAST instrumentation for org.apache.http.client.HttpClient

Motivation
Add SSRF support for Apache HttpClient V4

Additional Notes
ApacheHttpClientInstrumentation

As org.apache.http.client.HttpClient is currently instrumented for tracing implementing Instrumenter.CanShortcutTypeMatching we will keep the same approach
  • Loading branch information
jandro996 committed Dec 19, 2023
1 parent 030073d commit 7574c1f
Show file tree
Hide file tree
Showing 17 changed files with 777 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package com.datadog.iast.sink;

import static datadog.trace.api.iast.VulnerabilityMarks.NOT_MARKED;

import com.datadog.iast.Dependencies;
import com.datadog.iast.IastRequestContext;
import com.datadog.iast.model.Evidence;
import com.datadog.iast.model.Range;
import com.datadog.iast.model.Source;
import com.datadog.iast.model.VulnerabilityType;
import com.datadog.iast.overhead.Operations;
import com.datadog.iast.taint.Ranges;
import com.datadog.iast.taint.TaintedObject;
import datadog.trace.api.iast.sink.SsrfModule;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
Expand All @@ -26,4 +34,53 @@ public void onURLConnection(@Nullable final Object url) {
}
checkInjection(span, ctx, VulnerabilityType.SSRF, url);
}

/**
* if the host or the uri are tainted, we report the url as tainted as well a new range is created
* covering all the value string in order to simplify the algorithm
*
* @param value
* @param host
* @param uri
*/
@Override
public void onURLConnection(@Nullable String value, @Nullable Object host, @Nullable Object uri) {
if (value == null) {
return;
}
final AgentSpan span = AgentTracer.activeSpan();
final IastRequestContext ctx = IastRequestContext.get(span);
if (ctx == null) {
return;
}
TaintedObject taintedObject = getTaintedObject(ctx, host, uri);
if (taintedObject == null) {
return;
}
Range[] ranges =
Ranges.getNotMarkedRanges(taintedObject.getRanges(), VulnerabilityType.SSRF.mark());
if (ranges == null || ranges.length == 0) {
return;
}
if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) {
return;
}
Source source = Ranges.highestPriorityRange(ranges).getSource();
final Evidence result =
new Evidence(value, new Range[] {new Range(0, value.length(), source, NOT_MARKED)});
report(span, VulnerabilityType.SSRF, result);
}

@Nullable
private TaintedObject getTaintedObject(
final IastRequestContext ctx, @Nullable final Object host, @Nullable final Object uri) {
TaintedObject taintedObject = null;
if (uri != null) {
taintedObject = ctx.getTaintedObjects().get(uri);
}
if (taintedObject == null && host != null) {
taintedObject = ctx.getTaintedObjects().get(host);
}
return taintedObject;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,42 @@ class SsrfModuleTest extends IastModuleImplTestBase {
0 * reporter.report(_, _)
}

void 'test SSRF detection for host and uri'() {
when:
module.onURLConnection(value, host, uri)

then: 'report is not called if no active span'
tracer.activeSpan() >> null
0 * reporter.report(_, _)

when:
module.onURLConnection(value, host, uri)

then: 'report is not called if host or uri are not tainted'
tracer.activeSpan() >> span
0 * reporter.report(_, _)

when:
taint(host!=null ? host : uri)
module.onURLConnection(value, host, uri)

then: 'report is called when the host or uri are tainted'
tracer.activeSpan() >> span
1 * reporter.report(span, {
Vulnerability vul -> vul.type == VulnerabilityType.SSRF
&& vul.evidence.value == value
&& vul.evidence.ranges.length == 1
&& vul.evidence.ranges[0].start == 0
&& vul.evidence.ranges[0].length == value.length()
})


where:
value | host | uri
'http://test.com' | new Object() | new URI('http://test.com/tested')
'http://test.com' | null | new URI('http://test.com/tested')
}

private void taint(final Object value) {
ctx.getTaintedObjects().taint(value, Ranges.forObject(new Source(SourceTypes.REQUEST_PARAMETER_VALUE, 'name', value.toString())))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.datadog.iast.test

import com.datadog.iast.IastRequestContext
import com.datadog.iast.model.Vulnerability
import com.datadog.iast.taint.TaintedObjects
import datadog.trace.agent.test.base.WithHttpServer
import datadog.trace.agent.tooling.bytebuddy.iast.TaintableVisitor
import datadog.trace.api.gateway.IGSpanInfo
import datadog.trace.api.gateway.RequestContext
import groovy.json.JsonBuilder
import groovy.transform.CompileStatic

import java.util.concurrent.LinkedBlockingQueue
Expand All @@ -15,10 +17,12 @@ abstract class IastHttpServerTest<SERVER> extends WithHttpServer<SERVER> impleme

private static final LinkedBlockingQueue<TaintedObjectCollection> TAINTED_OBJECTS = new LinkedBlockingQueue<>()

private static final LinkedBlockingQueue<List<Vulnerability>> VULNERABILITIES = new LinkedBlockingQueue<>()

@CompileStatic
void configurePreAgent() {
super.configurePreAgent()
injectSysConfig('dd.iast.enabled', 'true')
super.configurePreAgent()
}

protected Closure getRequestEndAction() {
Expand All @@ -28,6 +32,8 @@ abstract class IastHttpServerTest<SERVER> extends WithHttpServer<SERVER> impleme
if (iastRequestContext) {
TaintedObjects taintedObjects = iastRequestContext.getTaintedObjects()
TAINTED_OBJECTS.offer(new TaintedObjectCollection(taintedObjects))
List<Vulnerability> vulns = iastRequestContext.getVulnerabilityBatch().getVulnerabilities()
VULNERABILITIES.offer(vulns)
}
}
}
Expand Down Expand Up @@ -61,4 +67,11 @@ abstract class IastHttpServerTest<SERVER> extends WithHttpServer<SERVER> impleme
String operation() {
return null
}

protected void hasVulnerability(final Closure<Boolean> matcher) {
List<Vulnerability> vulns = VULNERABILITIES.poll(15, TimeUnit.SECONDS)
if(vulns.find(matcher) == null){
throw new AssertionError("No matching vulnerability found. Vulnerabilities found: ${new JsonBuilder(vulns).toPrettyString()}")
}
}
}
23 changes: 22 additions & 1 deletion dd-java-agent/instrumentation/apache-httpclient-4/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,32 @@ muzzle {
apply from: "$rootDir/gradle/java.gradle"

addTestSuiteForDir('latestDepTest', 'test')
addTestSuite('iastIntegrationTest')
addTestSuiteExtendingForDir('v41IastIntegrationTest', 'iastIntegrationTest', 'iastIntegrationTest')
addTestSuiteExtendingForDir('v42IastIntegrationTest', 'iastIntegrationTest', 'iastIntegrationTest')
addTestSuiteExtendingForDir('v43IastIntegrationTest', 'iastIntegrationTest', 'iastIntegrationTest')
addTestSuiteExtendingForDir('v44IastIntegrationTest', 'iastIntegrationTest', 'iastIntegrationTest')
addTestSuiteExtendingForDir('v45IastIntegrationTest', 'iastIntegrationTest', 'iastIntegrationTest')

dependencies {
compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'

testImplementation(testFixtures(project(':dd-java-agent:agent-iast')))
testImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'

// to instrument the integration test
iastIntegrationTestImplementation(testFixtures(project(':dd-java-agent:agent-iast')))
iastIntegrationTestImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.0'
iastIntegrationTestRuntimeOnly(project(':dd-java-agent:instrumentation:jetty-9'))
iastIntegrationTestRuntimeOnly(project(':dd-java-agent:instrumentation:apache-httpcore-4'))
iastIntegrationTestRuntimeOnly(project(':dd-java-agent:instrumentation:servlet'))
iastIntegrationTestRuntimeOnly(project(':dd-java-agent:instrumentation:java-net'))
iastIntegrationTestRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter')

v41IastIntegrationTestImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.1'
v42IastIntegrationTestImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.2'
v43IastIntegrationTestImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.3'
v44IastIntegrationTestImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.4'
v45IastIntegrationTestImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5'

latestDepTestImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '+'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import com.datadog.iast.model.VulnerabilityType
import com.datadog.iast.test.IastHttpServerTest
import datadog.trace.agent.test.base.HttpServer
import okhttp3.Request

import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer

class IastHttpClientIntegrationTest extends IastHttpServerTest<HttpServer> {

static final CLIENTS = [
'org.apache.http.impl.client.AutoRetryHttpClient',
'org.apache.http.impl.client.ContentEncodingHttpClient',
'org.apache.http.impl.client.DefaultHttpClient',
'org.apache.http.impl.client.SystemDefaultHttpClient'
]

@Override
HttpServer server() {
final controller = new SsrfController()
return httpServer {
handlers {
prefix('/ssrf/execute') {
final msg = controller.apacheSsrf(
(String) request.getParameter('url'),
(String) request.getParameter('clientClassName'),
(String) request.getParameter('method'),
(String) request.getParameter('requestType'),
(String) request.getParameter('scheme')
)
response.status(200).send(msg)
}
}
}.asHttpServer()
}

void 'ssrf is present'() {
setup:
def expected = 'http://inexistent/test/' + suite.executedMethod
if (suite.scheme == 'https') {
expected = expected.replace('http', 'https')
}
final url = address.toString() + 'ssrf/execute' + '?url=' + expected + '&clientClassName=' + suite.clientImplementation + '&method=' + suite.executedMethod + '&requestType=' + suite.requestType + '&scheme=' + suite.scheme
final request = new Request.Builder().url(url).get().build()


when:
def response = client.newCall(request).execute()

then:
response.successful
TEST_WRITER.waitForTraces(1)
def to = getFinReqTaintedObjects()
assert to != null
hasVulnerability (
vul ->
vul.type == VulnerabilityType.SSRF
&& vul.evidence.value == expected
)


where:
suite << createTestSuite()
}

private Iterable<TestSuite> createTestSuite() {
final result = []
for (String client : getAvailableClientsClassName()) {
for (SsrfController.ExecuteMethod method : SsrfController.ExecuteMethod.values()) {
if (method.name().startsWith('HOST')) {
result.add(createTestSuite(client, method, SsrfController.Request.HttpRequest, 'http'))
result.add(createTestSuite(client, method, SsrfController.Request.HttpRequest, 'https'))
}
result.add(createTestSuite(client, method, SsrfController.Request.HttpUriRequest, 'http'))
}
}
return result as Iterable<TestSuite>
}

private TestSuite createTestSuite(client, method, request, scheme) {
return new TestSuite(
description: "ssrf is present for ${client} client and ${method} method with ${request} and ${scheme} scheme",
executedMethod: method.name(),
clientImplementation: client,
requestType: request.name(),
scheme: scheme
)
}

private String[] getAvailableClientsClassName() {
def availableClients = []
CLIENTS.each {
try {
Class.forName(it)
availableClients.add(it)
} catch (ClassNotFoundException e) {
// ignore
}
}
return availableClients
}

private static class TestSuite {
String description
String executedMethod
String clientImplementation
String requestType
String scheme

@Override
String toString() {
return "IAST apache httpclient 4 test suite: ${description}"
}
}
}
Loading

0 comments on commit 7574c1f

Please sign in to comment.