Skip to content

Commit 7ab7c4f

Browse files
authored
Add Thymeleaf support to IAST XSS vulnerability (#5901)
#What Does This Do Instrument Thymeleaf classes that allow template generation avoiding escape functions #Additional Notes Is not possible to get tainted tex, templateName and line in the same class, so two instrumentation sharing context are needed. org.thymeleaf.standard.processor.StandardUtextTagProcessor#doProcess -> Set IElementTagStructureHandler as potentially dangerous storing it with template name and line in the instrumentation context org.thymeleaf.processor.element.IElementTagStructureHandler#setBody -> if is in the instrumentation context check if the input is tainted to report the vulnerability
1 parent 1659cd3 commit 7ab7c4f

File tree

17 files changed

+406
-1
lines changed

17 files changed

+406
-1
lines changed

dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/Location.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public static Location forSpanAndClassAndMethod(
2626
return new Location(spanId, clazz, -1, method);
2727
}
2828

29+
public static Location forSpanAndFileAndLine(
30+
final long spanId, final String file, final int line) {
31+
return new Location(spanId, file, line, null);
32+
}
33+
2934
public long getSpanId() {
3035
return spanId == null ? 0 : spanId;
3136
}

dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/XssModuleImpl.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void onXss(@Nonnull String s, @Nonnull String clazz, @Nonnull String meth
5656
if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) {
5757
return;
5858
}
59-
final Evidence evidence = new Evidence(s.toString(), notMarkedRanges);
59+
final Evidence evidence = new Evidence(s, notMarkedRanges);
6060
reporter.report(
6161
span,
6262
new Vulnerability(
@@ -92,4 +92,35 @@ public void onXss(@Nonnull String format, @Nullable Object[] args) {
9292
checkInjection(
9393
span, VulnerabilityType.XSS, rangesProviderFor(to, format), rangesProviderFor(to, args));
9494
}
95+
96+
@Override
97+
public void onXss(@Nonnull CharSequence s, @Nullable String file, int line) {
98+
if (!canBeTainted(s) || file == null || file.isEmpty()) {
99+
return;
100+
}
101+
final AgentSpan span = AgentTracer.activeSpan();
102+
final IastRequestContext ctx = IastRequestContext.get(span);
103+
if (ctx == null) {
104+
return;
105+
}
106+
TaintedObject taintedObject = ctx.getTaintedObjects().get(s);
107+
if (taintedObject == null) {
108+
return;
109+
}
110+
Range[] notMarkedRanges =
111+
Ranges.getNotMarkedRanges(taintedObject.getRanges(), VulnerabilityType.XSS.mark());
112+
if (notMarkedRanges == null || notMarkedRanges.length == 0) {
113+
return;
114+
}
115+
if (!overheadController.consumeQuota(Operations.REPORT_VULNERABILITY, span)) {
116+
return;
117+
}
118+
final Evidence evidence = new Evidence(s.toString(), notMarkedRanges);
119+
reporter.report(
120+
span,
121+
new Vulnerability(
122+
VulnerabilityType.XSS,
123+
Location.forSpanAndFileAndLine(span.getSpanId(), file, line),
124+
evidence));
125+
}
95126
}

dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/XssModuleTest.groovy

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,30 @@ class XssModuleTest extends IastModuleImplTestBase {
119119
'/==>var<==' | ['a', 'b'] | VulnerabilityMarks.SQL_INJECTION_MARK | "/==>var<== a b"
120120
}
121121

122+
void 'module detects Charsequence XSS with file and line'() {
123+
setup:
124+
final param = mapTainted(s, mark)
125+
126+
when:
127+
module.onXss(param as CharSequence, file as String, line as int)
128+
129+
then:
130+
if (expected != null) {
131+
1 * reporter.report(_, _) >> { args -> assertEvidence(args[1] as Vulnerability, expected) }
132+
} else {
133+
0 * reporter.report(_, _)
134+
}
135+
136+
where:
137+
s | file | line | mark | expected
138+
null | 'test' | 3 | NOT_MARKED | null
139+
'/var' | 'test' | 3 | NOT_MARKED | null
140+
'/==>var<=='| 'test' | 3 | NOT_MARKED | "/==>var<=="
141+
'/==>var<=='| 'test' | 3 | VulnerabilityMarks.XSS_MARK | null
142+
'/==>var<=='| 'test' | 3 | VulnerabilityMarks.SQL_INJECTION_MARK | "/==>var<=="
143+
'/==>var<=='| null | 3 | VulnerabilityMarks.SQL_INJECTION_MARK | null
144+
}
145+
122146
void 'iast module detects String xss with class and method (#value)'() {
123147
setup:
124148
final param = mapTainted(value, mark)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
muzzle {
2+
pass {
3+
group = 'org.thymeleaf'
4+
module = 'thymeleaf'
5+
versions = '[3.0.0.RELEASE,]'
6+
assertInverse = true
7+
}
8+
}
9+
10+
apply from: "$rootDir/gradle/java.gradle"
11+
12+
addTestSuiteForDir('latestDepTest', 'test')
13+
14+
dependencies {
15+
16+
compileOnly group: 'org.thymeleaf', name: 'thymeleaf', version: '3.0.0.RELEASE'
17+
18+
testImplementation group: 'org.thymeleaf', name: 'thymeleaf', version: '3.0.0.RELEASE'
19+
20+
testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter')
21+
22+
latestDepTestImplementation group: 'org.thymeleaf', name: 'thymeleaf', version: '+'
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package datadog.trace.instrumentation.thymeleaf;
2+
3+
import datadog.trace.api.iast.InstrumentationBridge;
4+
import datadog.trace.api.iast.Sink;
5+
import datadog.trace.api.iast.VulnerabilityTypes;
6+
import datadog.trace.api.iast.sink.XssModule;
7+
import datadog.trace.bootstrap.ContextStore;
8+
import datadog.trace.bootstrap.InstrumentationContext;
9+
import net.bytebuddy.asm.Advice;
10+
import org.thymeleaf.engine.ElementTagStructureHandler;
11+
import org.thymeleaf.processor.element.IElementTagStructureHandler;
12+
13+
public class BodyAdvice {
14+
@Advice.OnMethodExit(suppress = Throwable.class)
15+
@Sink(VulnerabilityTypes.XSS)
16+
public static void setBody(
17+
@Advice.This ElementTagStructureHandler self, @Advice.Argument(0) final CharSequence text) {
18+
final XssModule module = InstrumentationBridge.XSS;
19+
if (module != null) {
20+
ContextStore<IElementTagStructureHandler, ThymeleafContext> contextStore =
21+
InstrumentationContext.get(IElementTagStructureHandler.class, ThymeleafContext.class);
22+
ThymeleafContext ctx = contextStore.get(self);
23+
if (ctx != null) {
24+
module.onXss(text, ctx.getTemplateName(), ctx.getLine());
25+
}
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package datadog.trace.instrumentation.thymeleaf;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static java.util.Collections.singletonMap;
5+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
6+
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
7+
8+
import com.google.auto.service.AutoService;
9+
import datadog.trace.agent.tooling.Instrumenter;
10+
import java.util.Map;
11+
12+
@AutoService(Instrumenter.class)
13+
public class ElementTagStructureHandlerInstrumentation extends Instrumenter.Iast
14+
implements Instrumenter.ForSingleType {
15+
public ElementTagStructureHandlerInstrumentation() {
16+
super("thymeleaf");
17+
}
18+
19+
@Override
20+
public String instrumentedType() {
21+
return "org.thymeleaf.engine.ElementTagStructureHandler";
22+
}
23+
24+
@Override
25+
public void adviceTransformations(AdviceTransformation transformation) {
26+
27+
transformation.applyAdvice(
28+
isMethod().and(named("setBody")).and(takesArgument(0, CharSequence.class)),
29+
packageName + ".BodyAdvice");
30+
}
31+
32+
@Override
33+
public String[] helperClassNames() {
34+
return new String[] {packageName + ".ThymeleafContext"};
35+
}
36+
37+
@Override
38+
public Map<String, String> contextStore() {
39+
return singletonMap(
40+
"org.thymeleaf.processor.element.IElementTagStructureHandler",
41+
"datadog.trace.instrumentation.thymeleaf.ThymeleafContext");
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package datadog.trace.instrumentation.thymeleaf;
2+
3+
import datadog.trace.api.iast.InstrumentationBridge;
4+
import datadog.trace.api.iast.Propagation;
5+
import datadog.trace.bootstrap.ContextStore;
6+
import datadog.trace.bootstrap.InstrumentationContext;
7+
import net.bytebuddy.asm.Advice;
8+
import org.thymeleaf.model.IProcessableElementTag;
9+
import org.thymeleaf.processor.element.IElementTagStructureHandler;
10+
11+
public class ProcessAdvice {
12+
13+
@Advice.OnMethodEnter(suppress = Throwable.class)
14+
@Propagation
15+
public static void doProcess(
16+
@Advice.Argument(1) final IProcessableElementTag tag,
17+
@Advice.Argument(4) final IElementTagStructureHandler handler) {
18+
if (InstrumentationBridge.XSS != null) {
19+
ContextStore<IElementTagStructureHandler, ThymeleafContext> contextStore =
20+
InstrumentationContext.get(IElementTagStructureHandler.class, ThymeleafContext.class);
21+
contextStore.put(handler, new ThymeleafContext(tag.getTemplateName(), tag.getLine()));
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package datadog.trace.instrumentation.thymeleaf;
2+
3+
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
4+
import static java.util.Collections.singletonMap;
5+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
6+
7+
import com.google.auto.service.AutoService;
8+
import datadog.trace.agent.tooling.Instrumenter;
9+
import java.util.Map;
10+
11+
@AutoService(Instrumenter.class)
12+
public class StandardUtextTagProcessorInstrumentation extends Instrumenter.Iast
13+
implements Instrumenter.ForSingleType {
14+
15+
public StandardUtextTagProcessorInstrumentation() {
16+
super("thymeleaf");
17+
}
18+
19+
@Override
20+
public void adviceTransformations(AdviceTransformation transformation) {
21+
transformation.applyAdvice(isMethod().and(named("doProcess")), packageName + ".ProcessAdvice");
22+
}
23+
24+
@Override
25+
public String[] helperClassNames() {
26+
return new String[] {packageName + ".ThymeleafContext"};
27+
}
28+
29+
@Override
30+
public Map<String, String> contextStore() {
31+
return singletonMap(
32+
"org.thymeleaf.processor.element.IElementTagStructureHandler",
33+
"datadog.trace.instrumentation.thymeleaf.ThymeleafContext");
34+
}
35+
36+
@Override
37+
public String instrumentedType() {
38+
return "org.thymeleaf.standard.processor.StandardUtextTagProcessor";
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package datadog.trace.instrumentation.thymeleaf;
2+
3+
public class ThymeleafContext {
4+
5+
private final String templateName;
6+
7+
private final int line;
8+
9+
public ThymeleafContext(final String file, final int line) {
10+
this.templateName = file;
11+
this.line = line;
12+
}
13+
14+
public String getTemplateName() {
15+
return templateName;
16+
}
17+
18+
public int getLine() {
19+
return line;
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package datadog.trace.instrumentation.thymeleaf
2+
3+
import datadog.trace.agent.test.AgentTestRunner
4+
import datadog.trace.api.iast.InstrumentationBridge
5+
import datadog.trace.api.iast.sink.XssModule
6+
import org.thymeleaf.TemplateEngine
7+
import org.thymeleaf.context.Context
8+
9+
class ThymeleafXssTest extends AgentTestRunner {
10+
11+
@Override
12+
protected void configurePreAgent() {
13+
injectSysConfig("dd.iast.enabled", "true")
14+
}
15+
16+
void 'test that XssModule is called' (){
17+
given:
18+
XssModule xssModule = Mock(XssModule)
19+
if(hasModule){
20+
InstrumentationBridge.registerIastModule(xssModule)
21+
}
22+
final engine = new TemplateEngine()
23+
final context = new Context(Locale.getDefault(), [q:'this is vulnerable'])
24+
25+
when:
26+
engine.process(template, context)
27+
28+
then:
29+
expected * xssModule.onXss('this is vulnerable', template, 1)
30+
0 * _
31+
32+
where:
33+
hasModule | template | expected
34+
false | '<span th:utext="${q}"></span>' | 0
35+
true | '<span th:utext="${q}"></span>' | 1
36+
true | '<span th:text="${q}"></span>' | 0
37+
}
38+
}

0 commit comments

Comments
 (0)