Skip to content

Commit 679f9e2

Browse files
Propagation of translateEscapes of String class (#8186)
1 parent 3806d93 commit 679f9e2

File tree

14 files changed

+353
-0
lines changed

14 files changed

+353
-0
lines changed

dd-java-agent/agent-iast/src/main/java/com/datadog/iast/propagation/StringModuleImpl.java

+21
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,27 @@ public void onStringJoin(
293293
}
294294
}
295295

296+
@Override
297+
@SuppressFBWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
298+
public void onStringTranslateEscapes(@Nonnull String self, @Nullable String result) {
299+
if (!canBeTainted(result)) {
300+
return;
301+
}
302+
if (self == result) { // same ref, no change in taint status
303+
return;
304+
}
305+
final IastContext ctx = IastContext.Provider.get();
306+
if (ctx == null) {
307+
return;
308+
}
309+
final TaintedObjects taintedObjects = ctx.getTaintedObjects();
310+
final TaintedObject taintedSelf = taintedObjects.get(self);
311+
if (taintedSelf == null) {
312+
return; // original string is not tainted
313+
}
314+
taintedObjects.taint(result, taintedSelf.getRanges()); // only possibility left
315+
}
316+
296317
@Override
297318
@SuppressFBWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
298319
public void onStringRepeat(@Nonnull String self, int count, @Nonnull String result) {

dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy

+22
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpan
1212
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
1313
import groovy.transform.CompileDynamic
1414
import org.junit.jupiter.api.Assertions
15+
import spock.lang.IgnoreIf
1516

1617
import java.text.SimpleDateFormat
1718

@@ -1414,6 +1415,27 @@ class StringModuleTest extends IastModuleImplTestBase {
14141415
taintFormat(result, taintedObject.getRanges()) == "==>my_input<=="
14151416
}
14161417
1418+
@IgnoreIf({ System.getProperty('java.specification.version').toBigDecimal() < 15 })
1419+
void 'test translate escapes'() {
1420+
given:
1421+
final taintedObjects = ctx.getTaintedObjects()
1422+
def self = addFromTaintFormat(taintedObjects, testString)
1423+
def result = self.translateEscapes()
1424+
1425+
when:
1426+
module.onStringTranslateEscapes(self, result)
1427+
def taintedObject = taintedObjects.get(result)
1428+
1429+
then:
1430+
taintFormat(result, taintedObject.getRanges()) == expected
1431+
1432+
where:
1433+
testString | expected
1434+
"==>hello world\t<==" | "==>hello world\t<=="
1435+
"==>hello world\n<==" | "==>hello world\n<=="
1436+
"==>hello worldn<==" | "==>hello worldn<=="
1437+
}
1438+
14171439
void 'test valueOf with special objects and make sure IastRequestContext is called'() {
14181440
given:
14191441
final taintedObjects = ctx.getTaintedObjects()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
plugins {
2+
id 'idea'
3+
}
4+
5+
ext {
6+
minJavaVersionForTests = JavaVersion.VERSION_15
7+
}
8+
9+
apply from: "$rootDir/gradle/java.gradle"
10+
apply plugin: 'call-site-instrumentation'
11+
12+
muzzle {
13+
pass {
14+
coreJdk()
15+
}
16+
}
17+
18+
idea {
19+
module {
20+
jdkName = '17'
21+
}
22+
}
23+
24+
csi {
25+
javaVersion = JavaLanguageVersion.of(17)
26+
}
27+
28+
addTestSuiteForDir('latestDepTest', 'test')
29+
30+
dependencies {
31+
testRuntimeOnly project(':dd-java-agent:instrumentation:iast-instrumenter')
32+
}
33+
34+
project.tasks.withType(AbstractCompile).configureEach {
35+
setJavaVersion(it, 17)
36+
if (it.name != 'compileCsiJava') {
37+
sourceCompatibility = JavaVersion.VERSION_15
38+
targetCompatibility = JavaVersion.VERSION_15
39+
if (it instanceof JavaCompile) {
40+
it.options.release.set(15)
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package datadog.trace.instrumentation.java.lang.jdk15;
2+
3+
import datadog.trace.agent.tooling.csi.CallSite;
4+
import datadog.trace.api.iast.IastCallSites;
5+
import datadog.trace.api.iast.InstrumentationBridge;
6+
import datadog.trace.api.iast.Propagation;
7+
import datadog.trace.api.iast.propagation.StringModule;
8+
9+
@Propagation
10+
@CallSite(
11+
spi = IastCallSites.class,
12+
enabled = {"datadog.trace.api.iast.IastEnabledChecks", "isMajorJavaVersionAtLeast", "15"})
13+
public class StringCallSite {
14+
@CallSite.After("java.lang.String java.lang.String.translateEscapes()")
15+
public static String afterTranslateEscapes(
16+
@CallSite.This final String self, @CallSite.Return final String result) {
17+
final StringModule module = InstrumentationBridge.STRING;
18+
try {
19+
if (module != null) {
20+
module.onStringTranslateEscapes(self, result);
21+
}
22+
} catch (final Throwable e) {
23+
module.onUnexpectedException("afterTranslateEscapes threw", e);
24+
}
25+
return result;
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package datadog.trace.instrumentation.java.lang.jdk15
2+
3+
import com.github.javaparser.utils.StringEscapeUtils
4+
import datadog.trace.agent.test.AgentTestRunner
5+
import datadog.trace.api.iast.InstrumentationBridge
6+
import datadog.trace.api.iast.propagation.StringModule
7+
import foo.bar.TestStringJDK15Suite
8+
import spock.lang.Requires
9+
10+
@Requires({
11+
jvm.java15Compatible
12+
})
13+
class StringCallSiteTest extends AgentTestRunner {
14+
15+
@Override
16+
protected void configurePreAgent() {
17+
injectSysConfig("dd.iast.enabled", "true")
18+
}
19+
20+
def 'test string translate escapes call site'() {
21+
setup:
22+
final iastModule = Mock(StringModule)
23+
InstrumentationBridge.registerIastModule(iastModule)
24+
25+
when:
26+
final result = TestStringJDK15Suite.stringTranslateEscapes(input)
27+
28+
then:
29+
result == output
30+
1 * iastModule.onStringTranslateEscapes(input, output)
31+
32+
where:
33+
input | output
34+
"HelloThisisaline" | "HelloThisisaline"
35+
"Hello\tThis is a line" | "Hello"+ StringEscapeUtils.unescapeJava("\\u0009") +"This is a line"
36+
/Hello\sThis is a line/ | "Hello"+ StringEscapeUtils.unescapeJava("\\u0020") +"This is a line"
37+
/Hello\"This is a line/ | "Hello"+ StringEscapeUtils.unescapeJava("\\u0022") +"This is a line"
38+
/Hello\0This is a line/ | "Hello"+ StringEscapeUtils.unescapeJava("\\u0000") +"This is a line"
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package foo.bar;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
public abstract class TestStringJDK15Suite {
7+
8+
private static final Logger LOGGER = LoggerFactory.getLogger(TestStringJDK15Suite.class);
9+
10+
private TestStringJDK15Suite() {}
11+
12+
public static String stringTranslateEscapes(String self) {
13+
LOGGER.debug("Before string translate escapes {}", self);
14+
final String result = self.translateEscapes();
15+
LOGGER.debug("After string translate escapes {}", result);
16+
return result;
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
plugins {
2+
id 'idea'
3+
id 'java-test-fixtures'
4+
}
5+
6+
7+
apply from: "$rootDir/gradle/java.gradle"
8+
9+
description = 'iast-smoke-tests-utils-java-17'
10+
11+
idea {
12+
module {
13+
jdkName = '17'
14+
}
15+
}
16+
17+
dependencies {
18+
api project(':dd-smoke-tests')
19+
compileOnly group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.2.0.RELEASE'
20+
21+
testFixturesImplementation testFixtures(project(":dd-smoke-tests:iast-util"))
22+
}
23+
24+
project.tasks.withType(AbstractCompile).configureEach {
25+
setJavaVersion(it, 17)
26+
sourceCompatibility = JavaVersion.VERSION_17
27+
targetCompatibility = JavaVersion.VERSION_17
28+
if (it instanceof JavaCompile) {
29+
it.options.release.set(17)
30+
}
31+
}
32+
33+
forbiddenApisMain {
34+
failOnMissingClasses = false
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package datadog.smoketest.springboot.controller;
2+
3+
import org.springframework.web.bind.annotation.PostMapping;
4+
import org.springframework.web.bind.annotation.RequestMapping;
5+
import org.springframework.web.bind.annotation.RequestParam;
6+
import org.springframework.web.bind.annotation.RestController;
7+
8+
@RestController
9+
@RequestMapping("/string")
10+
public class StringOperationController {
11+
12+
@PostMapping("/translateEscapes")
13+
public String translateEscapes(@RequestParam(value = "parameter") final String parameter) {
14+
parameter.translateEscapes();
15+
return "ok";
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package datadog.smoketest
2+
3+
import com.github.javaparser.utils.StringEscapeUtils
4+
import okhttp3.FormBody
5+
import okhttp3.Request
6+
7+
import static datadog.trace.api.config.IastConfig.IAST_DEBUG_ENABLED
8+
import static datadog.trace.api.config.IastConfig.IAST_DETECTION_MODE
9+
import static datadog.trace.api.config.IastConfig.IAST_ENABLED
10+
11+
abstract class AbstractIast17SpringBootTest extends AbstractIastServerSmokeTest {
12+
13+
@Override
14+
ProcessBuilder createProcessBuilder() {
15+
String springBootShadowJar = System.getProperty('datadog.smoketest.springboot.shadowJar.path')
16+
17+
List<String> command = []
18+
command.add(javaPath())
19+
command.addAll(defaultJavaProperties)
20+
command.addAll(iastJvmOpts())
21+
command.addAll((String[]) ['-jar', springBootShadowJar, "--server.port=${httpPort}"])
22+
ProcessBuilder processBuilder = new ProcessBuilder(command)
23+
processBuilder.directory(new File(buildDirectory))
24+
// Spring will print all environment variables to the log, which may pollute it and affect log assertions.
25+
processBuilder.environment().clear()
26+
return processBuilder
27+
}
28+
29+
protected List<String> iastJvmOpts() {
30+
return [
31+
withSystemProperty(IAST_ENABLED, true),
32+
withSystemProperty(IAST_DETECTION_MODE, 'FULL'),
33+
withSystemProperty(IAST_DEBUG_ENABLED, true),
34+
]
35+
}
36+
37+
void 'test String translateEscapes'() {
38+
setup:
39+
final url = "http://localhost:${httpPort}/string/translateEscapes"
40+
final body = new FormBody.Builder()
41+
.add('parameter', value)
42+
.build()
43+
final request = new Request.Builder().url(url).post(body).build()
44+
45+
46+
when:
47+
client.newCall(request).execute()
48+
49+
then:
50+
hasTainted { tainted ->
51+
tainted.value == expected
52+
}
53+
54+
where:
55+
value | expected
56+
"withEscape\ttab" | "withEscape" + Character.toString((char)9) + "tab"
57+
"withEscape\nnewline" | "withEscape" + StringEscapeUtils.unescapeJava("\\u000A")+ "newline"
58+
"withEscape\bbackline" | "withEscape" + StringEscapeUtils.unescapeJava("\\u0008")+ "backline"
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot' version '2.7.15'
4+
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
5+
id 'java-test-fixtures'
6+
}
7+
8+
ext {
9+
minJavaVersionForTests = JavaVersion.VERSION_17
10+
}
11+
12+
apply from: "$rootDir/gradle/java.gradle"
13+
description = 'SpringBoot Java 17 Smoke Tests.'
14+
15+
repositories {
16+
mavenCentral()
17+
}
18+
19+
dependencies {
20+
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.2.0.RELEASE'
21+
22+
testImplementation project(':dd-smoke-tests')
23+
testImplementation testFixtures(project(":dd-smoke-tests:iast-util:iast-util-17"))
24+
testImplementation testFixtures(project(':dd-smoke-tests:iast-util'))
25+
26+
implementation project(':dd-smoke-tests:iast-util:iast-util-17')
27+
}
28+
29+
project.tasks.withType(AbstractCompile).configureEach {
30+
setJavaVersion(it, 17)
31+
sourceCompatibility = JavaVersion.VERSION_17
32+
targetCompatibility = JavaVersion.VERSION_17
33+
if (it instanceof JavaCompile) {
34+
it.options.release.set(17)
35+
}
36+
}
37+
38+
forbiddenApisMain {
39+
failOnMissingClasses = false
40+
}
41+
42+
tasks.withType(Test).configureEach {
43+
dependsOn "bootJar"
44+
jvmArgs "-Ddatadog.smoketest.springboot.shadowJar.path=${tasks.bootJar.archiveFile.get()}"
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package datadog.smoketest.springboot;
2+
3+
import java.lang.management.ManagementFactory;
4+
import org.springframework.boot.SpringApplication;
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
7+
@SpringBootApplication
8+
public class SpringbootApplication {
9+
10+
public static void main(final String[] args) {
11+
SpringApplication.run(SpringbootApplication.class, args);
12+
System.out.println("Started in " + ManagementFactory.getRuntimeMXBean().getUptime() + "ms");
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package datadog.smoketest.springboot
2+
3+
import datadog.smoketest.AbstractIast17SpringBootTest
4+
5+
class IastSpringBootSmokeTest extends AbstractIast17SpringBootTest {
6+
}

internal-api/src/main/java/datadog/trace/api/iast/propagation/StringModule.java

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ void onStringJoin(
3535

3636
void onStringToUpperCase(@Nonnull String self, @Nullable String result);
3737

38+
void onStringTranslateEscapes(@Nonnull String self, @Nullable String result);
39+
3840
void onStringToLowerCase(@Nonnull String self, @Nullable String result);
3941

4042
void onStringTrim(@Nonnull String self, @Nullable String result);

0 commit comments

Comments
 (0)