diff --git a/dd-java-agent/instrumentation/freemarker-2.3.24/build.gradle b/dd-java-agent/instrumentation/freemarker-2.3.24/build.gradle index 0845f3a63b6..1e8b4cc0bfd 100644 --- a/dd-java-agent/instrumentation/freemarker-2.3.24/build.gradle +++ b/dd-java-agent/instrumentation/freemarker-2.3.24/build.gradle @@ -17,6 +17,7 @@ dependencies { compileOnly group: 'org.freemarker', name: 'freemarker', version: '2.3.24-incubating' testImplementation group: 'org.freemarker', name: 'freemarker', version: '2.3.24-incubating' + testImplementation project(':dd-java-agent:instrumentation:freemarker-2.3.9') testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter') diff --git a/dd-java-agent/instrumentation/freemarker-2.3.9/build.gradle b/dd-java-agent/instrumentation/freemarker-2.3.9/build.gradle new file mode 100644 index 00000000000..d94800ca4b6 --- /dev/null +++ b/dd-java-agent/instrumentation/freemarker-2.3.9/build.gradle @@ -0,0 +1,23 @@ +muzzle { + fail { + name = 'freemarker-2.3.9' + group = 'org.freemarker' + module = 'freemarker' + versions = '[2.3.24-incubating,]' + assertInverse = true + } +} + +apply from: "$rootDir/gradle/java.gradle" + +addTestSuiteForDir("version2_3_23Test", "test") + +dependencies { + compileOnly group: 'org.freemarker', name: 'freemarker', version: '2.3.9' + + testImplementation group: 'org.freemarker', name: 'freemarker', version: '2.3.9' + + testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter') + + version2_3_23TestImplementation group: 'org.freemarker', name: 'freemarker', version: '2.3.23' +} diff --git a/dd-java-agent/instrumentation/freemarker-2.3.9/src/main/java/datadog/trace/instrumentation/freemarker9/DollarVariableInstrumentation.java b/dd-java-agent/instrumentation/freemarker-2.3.9/src/main/java/datadog/trace/instrumentation/freemarker9/DollarVariableInstrumentation.java new file mode 100644 index 00000000000..efaa297a3f0 --- /dev/null +++ b/dd-java-agent/instrumentation/freemarker-2.3.9/src/main/java/datadog/trace/instrumentation/freemarker9/DollarVariableInstrumentation.java @@ -0,0 +1,81 @@ +package datadog.trace.instrumentation.freemarker9; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassNamed; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.iast.InstrumentationBridge; +import datadog.trace.api.iast.Sink; +import datadog.trace.api.iast.VulnerabilityTypes; +import datadog.trace.api.iast.sink.XssModule; +import freemarker.core.DollarVariable2_3_9Helper; +import freemarker.core.Environment; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public class DollarVariableInstrumentation extends InstrumenterModule.Iast + implements Instrumenter.ForSingleType { + static final String FREEMARKER_CORE = "freemarker.core"; + + public DollarVariableInstrumentation() { + super("freemarker"); + } + + @Override + public String muzzleDirective() { + return "freemarker-2.3.9"; + } + + static final ElementMatcher.Junction NOT_VERSION_PRIOR_2_3_24 = + not(hasClassNamed("freemarker.cache.ByteArrayTemplateLoader")); + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return NOT_VERSION_PRIOR_2_3_24; + } + + @Override + public String instrumentedType() { + return FREEMARKER_CORE + ".DollarVariable"; + } + + @Override + public String[] helperClassNames() { + return new String[] {FREEMARKER_CORE + ".DollarVariable2_3_9Helper"}; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + named("accept") + .and(isMethod()) + .and(takesArgument(0, named(FREEMARKER_CORE + ".Environment"))), + DollarVariableInstrumentation.class.getName() + "$DollarVariableAdvice"); + } + + public static class DollarVariableAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + @Sink(VulnerabilityTypes.XSS) + public static void onEnter( + @Advice.Argument(0) final Environment environment, @Advice.This final Object self) { + if (environment == null || self == null) { + return; + } + final XssModule xssModule = InstrumentationBridge.XSS; + if (xssModule == null) { + return; + } + String charSec = DollarVariable2_3_9Helper.fetchCharSec(self, environment); + final String templateName = environment.getTemplate().getName(); + final int line = DollarVariable2_3_9Helper.fetchBeginLine(self); + xssModule.onXss(charSec, templateName, line); + } + } +} diff --git a/dd-java-agent/instrumentation/freemarker-2.3.9/src/main/java/freemarker/core/DollarVariable2_3_9Helper.java b/dd-java-agent/instrumentation/freemarker-2.3.9/src/main/java/freemarker/core/DollarVariable2_3_9Helper.java new file mode 100644 index 00000000000..03d3d4b3b4e --- /dev/null +++ b/dd-java-agent/instrumentation/freemarker-2.3.9/src/main/java/freemarker/core/DollarVariable2_3_9Helper.java @@ -0,0 +1,59 @@ +package freemarker.core; + +import freemarker.template.TemplateModelException; +import java.lang.reflect.Field; +import java.lang.reflect.UndeclaredThrowableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class DollarVariable2_3_9Helper { + private DollarVariable2_3_9Helper() {} + + private static final Logger log = LoggerFactory.getLogger(DollarVariable2_3_9Helper.class); + + private static final Field ESCAPED_EXPRESSION = prepareEscapedExpression(); + + private static Field prepareEscapedExpression() { + try { + Field autoEscape = DollarVariable.class.getDeclaredField("escapedExpression"); + autoEscape.setAccessible(true); + return autoEscape; + } catch (Throwable e) { + log.debug("Failed to get DollarVariable escapedExpression", e); + return null; + } + } + + public static Expression fetchEscapeExpression(Object object) { + if (ESCAPED_EXPRESSION == null || !(object instanceof DollarVariable)) { + return null; + } + try { + return (Expression) ESCAPED_EXPRESSION.get(object); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } + } + + public static String fetchCharSec(Object object, Environment environment) { + if (!(object instanceof DollarVariable)) { + return null; + } + final Expression expression = DollarVariable2_3_9Helper.fetchEscapeExpression(object); + if (expression instanceof BuiltIn) { + return null; + } + try { + return environment.getDataModel().get(expression.toString()).toString(); + } catch (TemplateModelException e) { + throw new UndeclaredThrowableException(e); + } + } + + public static Integer fetchBeginLine(Object object) { + if (!(object instanceof DollarVariable)) { + return null; + } + return ((DollarVariable) object).beginLine; + } +} diff --git a/dd-java-agent/instrumentation/freemarker-2.3.9/src/test/groovy/datadog/trace/instrumentation/freemarker9/DollarVariableInstrumentationTest.groovy b/dd-java-agent/instrumentation/freemarker-2.3.9/src/test/groovy/datadog/trace/instrumentation/freemarker9/DollarVariableInstrumentationTest.groovy new file mode 100644 index 00000000000..cdcc9bf9a86 --- /dev/null +++ b/dd-java-agent/instrumentation/freemarker-2.3.9/src/test/groovy/datadog/trace/instrumentation/freemarker9/DollarVariableInstrumentationTest.groovy @@ -0,0 +1,38 @@ +package datadog.trace.instrumentation.freemarker9 + +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.iast.InstrumentationBridge +import datadog.trace.api.iast.sink.XssModule +import freemarker.template.Configuration +import freemarker.template.SimpleHash +import freemarker.template.Template +import freemarker.template.TemplateHashModel + +class DollarVariableInstrumentationTest extends AgentTestRunner { + + @Override + protected void configurePreAgent() { + injectSysConfig('dd.iast.enabled', 'true') + } + + void 'test freemarker process'() { + given: + final module = Mock(XssModule) + InstrumentationBridge.registerIastModule(module) + + final Configuration cfg = new Configuration() + final Template template = new Template("test", new StringReader("test \${$stringExpression}"), cfg) + final TemplateHashModel rootDataModel = new SimpleHash(cfg.getObjectWrapper()) + rootDataModel.put(stringExpression, expression) + + when: + template.process(rootDataModel, Mock(FileWriter)) + + then: + 1 * module.onXss(_, _, _) + + where: + stringExpression | expression + "test" | "test" + } +} diff --git a/settings.gradle b/settings.gradle index ae664549692..9de8095872e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -226,6 +226,7 @@ include ':dd-java-agent:instrumentation:elasticsearch:transport-7.3' include ':dd-java-agent:instrumentation:enable-wallclock-profiling' include ':dd-java-agent:instrumentation:exception-profiling' include ':dd-java-agent:instrumentation:finatra-2.9' +include ':dd-java-agent:instrumentation:freemarker-2.3.9' include ':dd-java-agent:instrumentation:freemarker-2.3.24' include ':dd-java-agent:instrumentation:glassfish' include ':dd-java-agent:instrumentation:google-http-client'