Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Taint gson sources (deserialization) v1.1 #6082

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8ea7e5d
First gson deserialization propagation version, tested for 1.1
jandro996 Oct 11, 2023
73e7f23
Refactor, current instrumentation only works for v1.1
jandro996 Oct 13, 2023
dde6d7a
Gson instrumentation for 1.4 and 1.5
jandro996 Oct 17, 2023
8c62450
Gson instrumentation from 1.6
jandro996 Oct 18, 2023
52e583d
Fix gradle
jandro996 Oct 18, 2023
c5e84cb
Update smoke test
jandro996 Oct 18, 2023
f98d8b8
fix spotless
jandro996 Oct 18, 2023
daeaeaa
fix muzzle
jandro996 Oct 18, 2023
1c24265
Merge branch 'master' into alejandro.gonzalez/Taint_gson_sources_dese…
jandro996 Oct 19, 2023
50813e8
remove unnecessary exclusions
jandro996 Oct 19, 2023
165bce8
Merge branch 'master' into alejandro.gonzalez/Taint_gson_sources_dese…
jandro996 Oct 19, 2023
4205f7c
Merge branch 'master' into alejandro.gonzalez/Taint_gson_sources_dese…
jandro996 Oct 23, 2023
c65ffa2
First gson deserialization propagation version, tested for 1.1
jandro996 Oct 11, 2023
8144934
Refactor, current instrumentation only works for v1.1
jandro996 Oct 13, 2023
83a0d41
Gson instrumentation for 1.4 and 1.5
jandro996 Oct 17, 2023
8999064
Gson instrumentation from 1.6
jandro996 Oct 18, 2023
b508a18
Introduce PII redaction based on keywords
jpbempel Oct 10, 2023
503d337
Throw exception on expression language in any case
jpbempel Oct 16, 2023
7cfac2a
remove unnecessary exclusions
jandro996 Oct 19, 2023
af5be91
Add instrumentation to constructor with InputStream and ReInit methods
jandro996 Oct 23, 2023
3d3f83e
Gson IAST source propagation for version 1.1
jandro996 Oct 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dd-java-agent/agent-jmxfetch/integrations-core
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@
0 com.google.common.base.internal.Finalizer
0 com.google.common.util.concurrent.*
2 com.google.gson.*
# Need for IAST: we instrument this class
0 com.google.gson.Gson
0 com.google.gson.JsonParser
0 com.google.gson.JsonParserJavacc
0 com.google.gson.stream.JsonReader
2 com.google.inject.*
# We instrument Runnable there
0 com.google.inject.internal.AbstractBindingProcessor$*
Expand Down
20 changes: 20 additions & 0 deletions dd-java-agent/instrumentation/gson-1.1/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
muzzle {
pass {
group = 'com.google.code.gson'
module = 'gson'
versions = '[1.1,1.4)'
assertInverse = true
}
}

apply from: "$rootDir/gradle/java.gradle"
apply plugin: 'call-site-instrumentation'


dependencies {
compileOnly group: 'com.google.code.gson', name: 'gson', version: '1.1'

testImplementation group: 'com.google.code.gson', name: 'gson', version: '1.1'

testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package datadog.trace.instrumentation.gson;

import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import com.google.gson.JsonPrimitive;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.muzzle.Reference;
import datadog.trace.api.iast.InstrumentationBridge;
import datadog.trace.api.iast.Propagation;
import datadog.trace.api.iast.propagation.PropagationModule;
import java.io.InputStream;
import net.bytebuddy.asm.Advice;

@AutoService(Instrumenter.class)
public class JsonParserInstrumentation extends Instrumenter.Iast
implements Instrumenter.ForSingleType {

@Override
public Reference[] additionalMuzzleReferences() {
return new Reference[] {
new Reference.Builder("com.google.gson.JsonParser")
.withMethod(new String[0], Reference.EXPECTS_PUBLIC, "<init>", "V", "Ljava/io/Reader;")
.build()
};
}

public JsonParserInstrumentation() {
super("gson");
}

@Override
public String instrumentedType() {
return "com.google.gson.JsonParser";
}

@Override
public void adviceTransformations(AdviceTransformation transformation) {
transformation.applyAdvice(
isConstructor().and(takesArguments(1)).and(takesArgument(0, named("java.io.Reader"))),
getClass().getName() + "$ConstructAdvice");
transformation.applyAdvice(
isConstructor().and(takesArguments(2)).and(takesArguments(InputStream.class, String.class)),
getClass().getName() + "$ConstructAdvice");
transformation.applyAdvice(
isMethod()
.and(named("ReInit"))
.and(takesArguments(1))
.and(takesArgument(0, named("java.io.Reader"))),
getClass().getName() + "$ConstructAdvice");
transformation.applyAdvice(
isMethod()
.and(named("ReInit"))
.and(takesArguments(2))
.and(takesArguments(InputStream.class, String.class)),
getClass().getName() + "$ConstructAdvice");
transformation.applyAdvice(
isMethod().and(named("JsonString")).and(takesArguments(0)),
getClass().getName() + "$ParseAdvice");
}

public static class ConstructAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
@Propagation
public static void afterInit(
@Advice.This Object self, @Advice.Argument(0) final java.lang.Object input) {
final PropagationModule iastModule = InstrumentationBridge.PROPAGATION;
if (iastModule != null && input != null) {
iastModule.taintIfInputIsTainted(self, input);
}
}
}

public static class ParseAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
@Propagation
public static void afterParse(
@Advice.This Object self, @Advice.Return final JsonPrimitive result) {
final PropagationModule iastModule = InstrumentationBridge.PROPAGATION;
if (iastModule != null && result != null) {
iastModule.taintIfInputIsTainted(result.getAsString(), self);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package datadog.trace.instrumentation.gson

import com.google.gson.Gson
import com.google.gson.JsonParser
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.iast.InstrumentationBridge
import datadog.trace.api.iast.propagation.PropagationModule

class JsonParserInstrumentationTest extends AgentTestRunner {

@Override
protected void configurePreAgent() {
injectSysConfig("dd.iast.enabled", "true")
}

void 'Test Gson instrumented'(){
given:
final gson = new Gson()

when:
final result = gson.fromJson('{"name": "nameTest", "value" : "valueTest"}', TestBean)

then:
result instanceof TestBean
result.getName() == 'nameTest'
result.getValue() == 'valueTest'
}


void 'test'() {
given:
final module = Mock(PropagationModule)
InstrumentationBridge.registerIastModule(module)
final input = inputClass.newInstance(inputClass == StringReader ? json : json.getBytes())

when:
final parser = new JsonParser(input)

then:
1 * module.taintIfInputIsTainted(_ as JsonParser, input)
0 * _

when:
parser.ReInit(input)

then:
1 * module.taintIfInputIsTainted(_ as JsonParser, input)
0 * _

when:
parser.parse()

then:
callsAfterParse * module.taintIfInputIsTainted(_ as String, _ as JsonParser)
0 * _

where:
json | inputClass | callsAfterParse
'"Test"' | StringReader | 1
'1'| StringReader| 0
'{"name": "nameTest", "value" : "valueTest"}'| StringReader| 4
'[{"name": "nameTest", "value" : "valueTest"}]'| StringReader| 4
'[{"name": "nameTest", "value" : "valueTest"}, {"name": "nameTest2", "value" : "valueTest2"}]'| StringReader | 8
'"Test"' | StringReader| 1
'1'| ByteArrayInputStream | 0
'{"name": "nameTest", "value" : "valueTest"}'| ByteArrayInputStream | 4
'[{"name": "nameTest", "value" : "valueTest"}]'| ByteArrayInputStream | 4
'[{"name": "nameTest", "value" : "valueTest"}, {"name": "nameTest2", "value" : "valueTest2"}]'| ByteArrayInputStream | 8
}


static final class TestBean {

private String name

private String value

String getName() {
return name
}

String getValue() {
return value
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
1 com.github.benmanes.caffeine.*
1 com.github.jknack.handlebars.*
1 com.google.*
#Need for gson propagation
2 com.google.gson.Gson
1 com.googlecode.*
1 com.hazelcast.*
1 com.hdivsecurity.*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package datadog.trace.instrumentation.java.lang;

import datadog.trace.agent.tooling.csi.CallSite;
import datadog.trace.api.iast.IastCallSites;
import datadog.trace.api.iast.InstrumentationBridge;
import datadog.trace.api.iast.Propagation;
import datadog.trace.api.iast.propagation.PropagationModule;
import java.io.StringReader;
import javax.annotation.Nonnull;

@Propagation
@CallSite(spi = IastCallSites.class)
public class StringReaderCallSite {

@CallSite.After("void java.io.StringReader.<init>(java.lang.String)")
public static StringReader afterInit(
@CallSite.AllArguments @Nonnull final Object[] params,
@CallSite.Return @Nonnull final StringReader result) {
final PropagationModule propagationModule = InstrumentationBridge.PROPAGATION;
if (propagationModule != null) {
propagationModule.taintIfInputIsTainted(result, params[0]);
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package datadog.trace.instrumentation.java.io

import datadog.trace.api.iast.InstrumentationBridge
import datadog.trace.api.iast.propagation.PropagationModule
import foo.bar.TestStringReaderSuite

class StringReaderCallSiteTest extends BaseIoCallSiteTest{

void 'test StringReader.<init>'(){
given:
PropagationModule iastModule = Mock(PropagationModule)
InstrumentationBridge.registerIastModule(iastModule)
final input = 'Test input'

when:
TestStringReaderSuite.init(input)

then:
1 * iastModule.taintIfInputIsTainted(_ as StringReader, input)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package foo.bar;

import java.io.StringReader;

public class TestStringReaderSuite {

public static void init(String input) {
new StringReader(input);
}
}
1 change: 1 addition & 0 deletions dd-smoke-tests/iast-util/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ description = 'iast-smoke-tests-utils'
dependencies {
api project(':dd-smoke-tests')
compileOnly group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '1.5.18.RELEASE'
compileOnly group: 'com.google.code.gson', name: 'gson', version: '1.1'
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package datadog.smoketest.springboot.controller;

import com.google.gson.Gson;
import datadog.smoketest.springboot.TestBean;
import ddtest.client.sources.Hasher;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
Expand Down Expand Up @@ -343,6 +344,13 @@ String pathInfo(HttpServletRequest request) {
return String.format("Request.getRequestURI returns %s", pathInfo);
}

@GetMapping("/gson_deserialization")
String gson(@RequestParam("json") String json) {
Gson gson = new Gson();
TestBean testBean = gson.fromJson(json, TestBean.class);
return "Test bean -> name: " + testBean.getName() + ", value: " + testBean.getValue();
}

private void withProcess(final Operation<Process> op) {
Process process = null;
try {
Expand Down
1 change: 1 addition & 0 deletions dd-smoke-tests/springboot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
implementation group: 'com.auth0', name: 'java-jwt', version: '4.0.0-beta.0'
implementation group: 'com.auth0', name: 'jwks-rsa', version: '0.21.1'
implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '9.22'
implementation group: 'com.google.code.gson', name: 'gson', version: '2.10'

implementation group: 'org.springframework', name: 'spring-web', version: '1.5.18.RELEASE'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,22 @@ class IastSpringBootSmokeTest extends AbstractIastSpringBootTest {
it.ranges[0].source.origin == 'http.request.header'
}
}


void 'gson deserialization'() {

given:
final url = "http://localhost:${httpPort}/gson_deserialization?json=%7B%22name%22%3A%20%22gsonTest%22%2C%20%22value%22%20%3A%20%22valueTest%22%7D"
final request = new Request.Builder().url(url).get().build()

when:
client.newCall(request).execute()

then:
hasTainted { tainted ->
tainted.value == 'gsonTest' &&
tainted.ranges[0].source.name == 'json' &&
tainted.ranges[0].source.origin == 'http.request.parameter'
}
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ include ':dd-java-agent:instrumentation:grizzly-2'
include ':dd-java-agent:instrumentation:grizzly-client-1.9'
include ':dd-java-agent:instrumentation:grizzly-http-2.3.20'
include ':dd-java-agent:instrumentation:grpc-1.5'
include ':dd-java-agent:instrumentation:gson-1.1'
include ':dd-java-agent:instrumentation:guava-10'
include ':dd-java-agent:instrumentation:hazelcast-3.6'
include ':dd-java-agent:instrumentation:hazelcast-3.9'
Expand Down
Loading