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

Fix jackson json parser propagation for field names #7606

Merged
merged 13 commits into from
Sep 30, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public abstract class NamedContext {

public abstract void taintName(@Nullable String name);

public abstract void setCurrentName(@Nullable final String name);

@Nonnull
public static <E> NamedContext getOrCreate(
@Nonnull final ContextStore<E, NamedContext> store, @Nonnull final E target) {
Expand Down Expand Up @@ -47,6 +49,9 @@ public void taintValue(@Nullable final String value) {}

@Override
public void taintName(@Nullable final String name) {}

@Override
public void setCurrentName(@Nullable final String name) {}
}

private static class NamedContextImpl extends NamedContext {
Expand Down Expand Up @@ -78,6 +83,11 @@ public void taintName(@Nullable final String name) {
}
}

@Override
public void setCurrentName(@Nullable final String name) {
currentName = name;
}

private IastContext iastCtx() {
if (!fetched) {
fetched = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ dependencies {
iastTestImplementation(testFixtures(project(':dd-java-agent:agent-iast')))
iastTestCompileOnly group: 'de.thetaphi', name: 'forbiddenapis', version: '3.4'
iastTestRuntimeOnly project(':dd-java-agent:instrumentation:jackson-core')
iastTestRuntimeOnly project(':dd-java-agent:instrumentation:jackson-core:jackson-core-2.8')
iastTestRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter')
iastTestRuntimeOnly project(':dd-java-agent:instrumentation:akka-http:akka-http-10.2-iast')

Expand Down Expand Up @@ -161,6 +162,7 @@ dependencies {
latestDepIastTestImplementation group: 'com.typesafe.akka', name: 'akka-actor_2.13', version: '2.8.+'
latestDepIastTestImplementation group: 'com.typesafe.akka', name: 'akka-http-jackson_2.13', version: '[10.+,10.5.2)'
latestDepIastTestImplementation(testFixtures(project(':dd-java-agent:agent-iast')))
latestDepIastTestImplementation project(':dd-java-agent:instrumentation:jackson-core:jackson-core-2.12')

lagomTestImplementation libs.scala211
lagomTestImplementation group: 'com.typesafe.akka', name: 'akka-http_2.11', version: '10.0.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@
1 graphql.*
1 ibm.security.*
1 io.dropwizard.*
2 io.ebean.*
2 io.ebeaninternal.*
1 io.github.lukehutch.fastclasspathscanner.*
1 io.grpc.*
1 io.leangen.geantyref.*
Expand Down
3 changes: 3 additions & 0 deletions dd-java-agent/instrumentation/jackson-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ dependencies {

testImplementation(group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion)
testImplementation(group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion)

latestDepTestImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.+'
latestDepTestImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.+'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
muzzle {
pass {
group = 'com.fasterxml.jackson.core'
module = 'jackson-core'
versions = "[2.12.0, 2.16.0)"
}
}

apply from: "$rootDir/gradle/java.gradle"

addTestSuiteForDir('latestDepTest', 'test')

final jacksonVersion = '2.12.0'
dependencies {
compileOnly(group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion)
compileOnly(group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion)

testImplementation(group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion)
testImplementation(group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion)

latestDepTestImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.15.+'
latestDepTestImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.+'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.fasterxml.jackson.core.json;

import com.fasterxml.jackson.core.sym.ByteQuadsCanonicalizer212Helper;

public final class JsonParser212Helper {
private JsonParser212Helper() {}

public static boolean fetchIntern(UTF8StreamJsonParser jsonParser) {
return ByteQuadsCanonicalizer212Helper.fetchIntern(jsonParser._symbols);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.fasterxml.jackson.core.sym;

public final class ByteQuadsCanonicalizer212Helper {
private ByteQuadsCanonicalizer212Helper() {}

public static boolean fetchIntern(ByteQuadsCanonicalizer symbols) {
return symbols._intern;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package datadog.trace.instrumentation.jackson_2_12.core;

import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed;
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.declaresMethod;
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.*;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.*;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.json.JsonParser212Helper;
import com.fasterxml.jackson.core.json.UTF8StreamJsonParser;
import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.api.iast.Propagation;
import datadog.trace.bootstrap.ContextStore;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.bootstrap.instrumentation.iast.NamedContext;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

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

static final String TARGET_TYPE = "com.fasterxml.jackson.core.JsonParser";
static final ElementMatcher.Junction<ClassLoader> VERSION_POST_2_8_0_AND_PRE_2_12_0 =
hasClassNamed("com.fasterxml.jackson.core.StreamReadCapability")
.and(not(hasClassNamed("com.fasterxml.jackson.core.StreamWriteConstraints")));

public JsonParserInstrumentation() {
super("jackson", "jackson-2_12");
}

@Override
public void methodAdvice(MethodTransformer transformer) {
final String className = JsonParserInstrumentation.class.getName();
transformer.applyAdvice(
namedOneOf("getCurrentName", "nextFieldName")
.and(isPublic())
.and(takesNoArguments())
.and(returns(String.class)),
className + "$NameAdvice");
}

@Override
public String hierarchyMarkerType() {
return TARGET_TYPE;
}

@Override
public ElementMatcher<TypeDescription> hierarchyMatcher() {
return declaresMethod(namedOneOf("getCurrentName", "nextFieldName"))
.and(
extendsClass(named(hierarchyMarkerType()))
.and(namedNoneOf("com.fasterxml.jackson.core.base.ParserMinimalBase")));
}

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return VERSION_POST_2_8_0_AND_PRE_2_12_0;
}

@Override
public Map<String, String> contextStore() {
return singletonMap(TARGET_TYPE, "datadog.trace.bootstrap.instrumentation.iast.NamedContext");
}

@Override
public String[] helperClassNames() {
return new String[] {
"com.fasterxml.jackson.core.json" + ".JsonParser212Helper",
"com.fasterxml.jackson.core.sym" + ".ByteQuadsCanonicalizer212Helper",
};
}

public static class NameAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
@Propagation
public static void onExit(@Advice.This JsonParser jsonParser, @Advice.Return String result) {
if (jsonParser != null
&& result != null
&& jsonParser.getCurrentToken() == JsonToken.FIELD_NAME) {
final ContextStore<JsonParser, NamedContext> store =
InstrumentationContext.get(JsonParser.class, NamedContext.class);
final NamedContext context = NamedContext.getOrCreate(store, jsonParser);
if (jsonParser instanceof UTF8StreamJsonParser
&& JsonParser212Helper.fetchIntern((UTF8StreamJsonParser) jsonParser)) {
context.setCurrentName(result);
return;
}
context.taintName(result);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package datadog.trace.instrumentation.jackson212.core

import com.fasterxml.jackson.databind.ObjectMapper
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.iast.InstrumentationBridge
import datadog.trace.api.iast.SourceTypes
import datadog.trace.api.iast.Taintable
import datadog.trace.api.iast.propagation.PropagationModule
import groovy.json.JsonOutput

import java.nio.charset.Charset

class JsonParserInstrumentationTest extends AgentTestRunner {

private final static String JSON_STRING = '{"root":"root_value","nested":["array_0","array_1"]}'

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

void 'test json parsing (tainted)'() {
given:
final source = new SourceImpl(origin: SourceTypes.REQUEST_BODY, name: 'body', value: JSON_STRING)
final module = Mock(PropagationModule)
InstrumentationBridge.registerIastModule(module)

and:
final reader = new ObjectMapper().readerFor(Map)

when:
final taintedResult = reader.readValue(target) as Map

then:
JsonOutput.toJson(taintedResult) == JSON_STRING
_ * module.taintObjectIfTainted(_, _)
_ * module.findSource(_) >> source
1 * module.taintString(_, 'root', source.origin, 'root', JSON_STRING)
1 * module.taintString(_, 'nested', source.origin, 'nested', JSON_STRING)
0 * _

where:
target << [JSON_STRING]
}

void 'test json parsing (tainted but field names)'() {
given:
final source = new SourceImpl(origin: SourceTypes.REQUEST_BODY, name: 'body', value: JSON_STRING)
final module = Mock(PropagationModule)
InstrumentationBridge.registerIastModule(module)

and:
final reader = new ObjectMapper()

when:
final taintedResult = reader.readValue(target, Map)

then:
JsonOutput.toJson(taintedResult) == JSON_STRING
_ * module.taintObjectIfTainted(_, _)
_ * module.findSource(_) >> source
0 * _

where:
target << [new ByteArrayInputStream(JSON_STRING.getBytes(Charset.defaultCharset()))]
}

void 'test json parsing (not tainted)'() {
given:
final module = Mock(PropagationModule)
InstrumentationBridge.registerIastModule(module)

and:
final reader = new ObjectMapper().readerFor(Map)

when:
final taintedResult = reader.readValue(target) as Map

then:
JsonOutput.toJson(taintedResult) == JSON_STRING
_ * module.taintObjectIfTainted(_, _)
_ * module.findSource(_) >> null
0 * _

where:
target << testSuite()
}

private static List<Object> testSuite() {
return [JSON_STRING, new ByteArrayInputStream(JSON_STRING.getBytes(Charset.defaultCharset()))]
}

private static class SourceImpl implements Taintable.Source {
byte origin
String name
String value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
muzzle {
pass {
group = 'com.fasterxml.jackson.core'
module = 'jackson-core'
versions = "[2.16.0,)"
}
}

apply from: "$rootDir/gradle/java.gradle"

addTestSuiteForDir('latestDepTest', 'test')

final jacksonVersion = '2.16.0'
dependencies {
compileOnly(group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion)
compileOnly(group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion)

testImplementation(group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion)
testImplementation(group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion)

latestDepTestImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.+'
latestDepTestImplementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.+'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.fasterxml.jackson.core.json;

import com.fasterxml.jackson.core.sym.ByteQuadsCanonicalizer216Helper;

public final class JsonParser216Helper {
private JsonParser216Helper() {}

public static boolean fetchInterner(UTF8StreamJsonParser jsonParser) {
return ByteQuadsCanonicalizer216Helper.fetchInterner(jsonParser._symbols);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.fasterxml.jackson.core.sym;

public final class ByteQuadsCanonicalizer216Helper {
private ByteQuadsCanonicalizer216Helper() {}

public static boolean fetchInterner(ByteQuadsCanonicalizer symbols) {
return symbols._interner != null;
}
}
Loading