Skip to content

Commit

Permalink
Add XSS support for Freemarker prior 2.3.24-incubating (#7497)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mariovido authored Sep 9, 2024
1 parent 3d46554 commit 3fd4174
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
23 changes: 23 additions & 0 deletions dd-java-agent/instrumentation/freemarker-2.3.9/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
Original file line number Diff line number Diff line change
@@ -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<ClassLoader> NOT_VERSION_PRIOR_2_3_24 =
not(hasClassNamed("freemarker.cache.ByteArrayTemplateLoader"));

@Override
public ElementMatcher.Junction<ClassLoader> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit 3fd4174

Please sign in to comment.