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 @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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)
}
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.ByteQuadsCanonicalizer2_12Helper;

public final class Json2_12ParserHelper {
Mariovido marked this conversation as resolved.
Show resolved Hide resolved
private Json2_12ParserHelper() {}

public static boolean fetchIntern(UTF8StreamJsonParser jsonParser) {
return ByteQuadsCanonicalizer2_12Helper.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 ByteQuadsCanonicalizer2_12Helper {
Mariovido marked this conversation as resolved.
Show resolved Hide resolved
private ByteQuadsCanonicalizer2_12Helper() {}

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.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.Json2_12ParserHelper;
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 Json2_12ParserInstrumentation extends InstrumenterModule.Iast
Mariovido marked this conversation as resolved.
Show resolved Hide resolved
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 Json2_12ParserInstrumentation() {
super("jackson", "jackson-2_12");
}

@Override
public void methodAdvice(MethodTransformer transformer) {
final String className = Json2_12ParserInstrumentation.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" + ".Json2_12ParserHelper",
"com.fasterxml.jackson.core.sym" + ".ByteQuadsCanonicalizer2_12Helper",
};
}

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
&& Json2_12ParserHelper.fetchIntern((UTF8StreamJsonParser) jsonParser)) {
context.setCurrentName(result);
return;
}
context.taintName(result);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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 Json12ParserInstrumentationTest extends AgentTestRunner {

private final static String JSON_STRING = '{"root":"root_value","nested":{"nested_array":["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)
// 1 * module.taintString(_, 'nested_array', source.origin, 'nested_array', JSON_STRING) --> TODO - CHECK WHY THIS IS NOT TAINTED
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,21 @@
muzzle {
pass {
group = 'com.fasterxml.jackson.core'
module = 'jackson-core'
versions = "[2.16.0,)"
// assertInverse = true
}
}

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)
}
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.ByteQuadsCanonicalizer2_16Helper;

public final class Json2_16ParserHelper {
Mariovido marked this conversation as resolved.
Show resolved Hide resolved
private Json2_16ParserHelper() {}

public static boolean fetchInterner(UTF8StreamJsonParser jsonParser) {
return ByteQuadsCanonicalizer2_16Helper.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 ByteQuadsCanonicalizer2_16Helper {
Mariovido marked this conversation as resolved.
Show resolved Hide resolved
private ByteQuadsCanonicalizer2_16Helper() {}

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