Skip to content

Commit 7bd70a5

Browse files
authored
Add IAST propagation to String valueOf (#8013)
1 parent 9eaf5a0 commit 7bd70a5

File tree

7 files changed

+211
-1
lines changed

7 files changed

+211
-1
lines changed

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

+50
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY;
99

1010
import com.datadog.iast.model.Range;
11+
import com.datadog.iast.model.Source;
1112
import com.datadog.iast.taint.Ranges;
1213
import com.datadog.iast.taint.TaintedObject;
1314
import com.datadog.iast.taint.TaintedObjects;
1415
import com.datadog.iast.util.RangeBuilder;
1516
import com.datadog.iast.util.Ranged;
1617
import com.datadog.iast.util.StringUtils;
1718
import datadog.trace.api.iast.IastContext;
19+
import datadog.trace.api.iast.Taintable;
1820
import datadog.trace.api.iast.propagation.StringModule;
1921
import de.thetaphi.forbiddenapis.SuppressForbidden;
2022
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -742,6 +744,54 @@ public String onStringReplace(
742744
numReplacements);
743745
}
744746

747+
@Override
748+
@SuppressFBWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
749+
public void onStringValueOf(Object param, @Nonnull String result) {
750+
if (param == null || !canBeTainted(result)) {
751+
return;
752+
}
753+
final IastContext ctx = IastContext.Provider.get();
754+
if (ctx == null) {
755+
return;
756+
}
757+
final TaintedObjects taintedObjects = ctx.getTaintedObjects();
758+
759+
if (param instanceof Taintable) {
760+
final Taintable taintable = (Taintable) param;
761+
if (!taintable.$DD$isTainted()) {
762+
return;
763+
}
764+
final Source source = (Source) taintable.$$DD$getSource();
765+
final Range[] ranges =
766+
Ranges.forCharSequence(
767+
result, new Source(source.getOrigin(), source.getName(), source.getValue()));
768+
769+
taintedObjects.taint(result, ranges);
770+
} else {
771+
final TaintedObject taintedParam = taintedObjects.get(param);
772+
if (taintedParam == null) {
773+
return;
774+
}
775+
776+
final Range[] rangesParam = taintedParam.getRanges();
777+
if (rangesParam.length == 0) {
778+
return;
779+
}
780+
781+
// Special objects like InputStream...
782+
if (rangesParam[0].getLength() == Integer.MAX_VALUE) {
783+
final Source source = rangesParam[0].getSource();
784+
final Range[] ranges =
785+
Ranges.forCharSequence(
786+
result, new Source(source.getOrigin(), source.getName(), source.getValue()));
787+
788+
taintedObjects.taint(result, ranges);
789+
} else {
790+
taintedObjects.taint(result, rangesParam);
791+
}
792+
}
793+
}
794+
745795
/**
746796
* Adds the tainted ranges belonging to the current parameter added via placeholder taking care of
747797
* an optional tainted placeholder.

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

+98-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.datadog.iast.propagation
22

33
import com.datadog.iast.IastModuleImplTestBase
4+
import com.datadog.iast.model.Source
5+
import com.datadog.iast.taint.TaintedObjects
46
import datadog.trace.api.gateway.RequestContext
57
import datadog.trace.api.gateway.RequestContextSlot
8+
import datadog.trace.api.iast.SourceTypes
9+
import datadog.trace.api.iast.Taintable
610
import datadog.trace.api.iast.propagation.StringModule
711
import datadog.trace.bootstrap.instrumentation.api.AgentSpan
812
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
@@ -16,6 +20,7 @@ import static com.datadog.iast.taint.TaintUtils.fromTaintFormat
1620
import static com.datadog.iast.taint.TaintUtils.getStringFromTaintFormat
1721
import static com.datadog.iast.taint.TaintUtils.taint
1822
import static com.datadog.iast.taint.TaintUtils.taintFormat
23+
import static com.datadog.iast.taint.TaintUtils.taintObject
1924

2025
@CompileDynamic
2126
class StringModuleTest extends IastModuleImplTestBase {
@@ -1298,6 +1303,65 @@ class StringModuleTest extends IastModuleImplTestBase {
12981303
"==>my_o<==u==>tput<====>my_o<==u==>tput<==" | 'out' | '==>in<==' | 0 | "==>my_o<==u==>tput<====>my_o<==u==>tput<=="
12991304
}
13001305
1306+
void 'test valueOf with (#param) and make sure IastRequestContext is called'() {
1307+
given:
1308+
final taintedObjects = ctx.getTaintedObjects()
1309+
def paramTainted = addFromTaintFormat(taintedObjects, param)
1310+
def result = String.valueOf(paramTainted)
1311+
1312+
when:
1313+
module.onStringValueOf(paramTainted, result)
1314+
def taintedObject = taintedObjects.get(result)
1315+
1316+
then:
1317+
1 * tracer.activeSpan() >> span
1318+
taintFormat(result, taintedObject.getRanges()) == expected
1319+
1320+
where:
1321+
param | expected
1322+
"==>test<==" | "==>test<=="
1323+
sb("==>test<==") | "==>test<=="
1324+
sbf("==>my_input<==") | "==>my_input<=="
1325+
}
1326+
1327+
void 'test valueOf with taintable object and make sure IastRequestContext is called'() {
1328+
given:
1329+
final taintedObjects = ctx.getTaintedObjects()
1330+
final source = taintedSource()
1331+
final param = taintable(taintedObjects, source)
1332+
final result = String.valueOf(param)
1333+
1334+
when:
1335+
module.onStringValueOf(param, result)
1336+
final taintedObject = taintedObjects.get(result)
1337+
1338+
then:
1339+
1 * tracer.activeSpan() >> span
1340+
taintFormat(result, taintedObject.getRanges()) == "==>my_input<=="
1341+
}
1342+
1343+
void 'test valueOf with special objects and make sure IastRequestContext is called'() {
1344+
given:
1345+
final taintedObjects = ctx.getTaintedObjects()
1346+
final source = taintedSource()
1347+
final param = new Object() {
1348+
@Override
1349+
String toString() {
1350+
return "my_input"
1351+
}
1352+
}
1353+
taintObject(taintedObjects, param, source)
1354+
final result = String.valueOf(param)
1355+
1356+
when:
1357+
module.onStringValueOf(param, result)
1358+
final taintedObject = taintedObjects.get(result)
1359+
1360+
then:
1361+
1 * tracer.activeSpan() >> span
1362+
taintFormat(result, taintedObject.getRanges()) == "==>my_input<=="
1363+
}
1364+
13011365
private static Date date(final String pattern, final String value) {
13021366
return new SimpleDateFormat(pattern).parse(value)
13031367
}
@@ -1310,11 +1374,44 @@ class StringModuleTest extends IastModuleImplTestBase {
13101374
return new StringBuilder(string)
13111375
}
13121376
1313-
private static StringBuilder sbf() {
1377+
private static StringBuffer sbf() {
13141378
return sbf('')
13151379
}
13161380
13171381
private static StringBuffer sbf(final String string) {
13181382
return new StringBuffer(string)
13191383
}
1384+
1385+
private static Source taintedSource(String value = 'value') {
1386+
return new Source(SourceTypes.REQUEST_PARAMETER_VALUE, 'name', value)
1387+
}
1388+
1389+
private static Taintable taintable(TaintedObjects tos, Source source = null) {
1390+
final result = new MockTaintable()
1391+
if (source != null) {
1392+
taintObject(tos, result, source)
1393+
}
1394+
return result
1395+
}
1396+
1397+
private static class MockTaintable implements Taintable {
1398+
private Source source
1399+
1400+
@SuppressWarnings('CodeNarc')
1401+
@Override
1402+
Source $$DD$getSource() {
1403+
return source
1404+
}
1405+
1406+
@SuppressWarnings('CodeNarc')
1407+
@Override
1408+
void $$DD$setSource(Source source) {
1409+
this.source = source
1410+
}
1411+
1412+
@Override
1413+
String toString() {
1414+
return "my_input"
1415+
}
1416+
}
13201417
}

dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/TaintUtils.groovy

+20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.datadog.iast.taint
33
import com.datadog.iast.model.Range
44
import com.datadog.iast.model.Source
55
import datadog.trace.api.iast.SourceTypes
6+
import datadog.trace.api.iast.Taintable
67

78
import static datadog.trace.api.iast.VulnerabilityMarks.NOT_MARKED
89

@@ -81,6 +82,14 @@ class TaintUtils {
8182
getStringFromTaintFormat(appendable.toString())
8283
}
8384

85+
static TaintedObject getTaintedObject(final TaintedObjects tos, final Object target) {
86+
if (target instanceof Taintable) {
87+
final source = (target as Taintable).$$DD$getSource() as Source
88+
return source == null ? null : new TaintedObject(target, Ranges.forObject(source))
89+
}
90+
return tos.get(target)
91+
}
92+
8493
static <E> E taint(final TaintedObjects tos, final E value) {
8594
if (value instanceof String) {
8695
return addFromTaintFormat(tos, value as String)
@@ -89,6 +98,17 @@ class TaintUtils {
8998
return value
9099
}
91100

101+
static TaintedObject taintObject(final TaintedObjects tos, final Object target, Source source, int mark = NOT_MARKED) {
102+
if (target instanceof Taintable) {
103+
target.$$DD$setSource(source)
104+
} else if (target instanceof CharSequence) {
105+
tos.taint(target, Ranges.forCharSequence(target, source, mark))
106+
} else {
107+
tos.taint(target, Ranges.forObject(source, mark))
108+
}
109+
return getTaintedObject(tos, target)
110+
}
111+
92112
static String addFromTaintFormat(final TaintedObjects tos, final String s) {
93113
return addFromTaintFormat(tos, s, NOT_MARKED)
94114
}

dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringCallSite.java

+14
Original file line numberDiff line numberDiff line change
@@ -300,4 +300,18 @@ public static String afterReplaceChar(
300300
}
301301
return result;
302302
}
303+
304+
@CallSite.After("java.lang.String java.lang.String.valueOf(java.lang.Object)")
305+
public static String afterValueOf(
306+
@CallSite.Argument(0) final Object obj, @CallSite.Return final String result) {
307+
final StringModule module = InstrumentationBridge.STRING;
308+
if (module != null) {
309+
try {
310+
module.onStringValueOf(obj, result);
311+
} catch (final Throwable e) {
312+
module.onUnexpectedException("afterValueOf threw", e);
313+
}
314+
}
315+
return result;
316+
}
303317
}

dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringCallSiteTest.groovy

+20
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,24 @@ class StringCallSiteTest extends AgentTestRunner {
265265
"test" | 't' | 'T' | "TesT"
266266
"test" | 'e' | 'E' | "tEst"
267267
}
268+
269+
def 'test string valueOf call site'() {
270+
setup:
271+
final stringModule = Mock(StringModule)
272+
InstrumentationBridge.registerIastModule(stringModule)
273+
274+
when:
275+
final result = TestStringSuite.valueOf(input)
276+
277+
then:
278+
result == expected
279+
1 * stringModule.onStringValueOf(input, expected)
280+
0 * _
281+
282+
where:
283+
input | expected
284+
"test" | "test"
285+
new StringBuilder("test") | "test"
286+
new StringBuffer("test") | "test"
287+
}
268288
}

dd-java-agent/instrumentation/java-lang/src/test/java/foo/bar/TestStringSuite.java

+7
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,11 @@ public static String replaceFirst(
239239
LOGGER.debug("After replace first {}", result);
240240
return result;
241241
}
242+
243+
public static String valueOf(final Object param) {
244+
LOGGER.debug("Before valueOf {}", param);
245+
String result = String.valueOf(param);
246+
LOGGER.debug("After valueOf {}", result);
247+
return result;
248+
}
242249
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,6 @@ void onStringFormat(
6161

6262
String onStringReplace(
6363
@Nonnull String self, String regex, String replacement, int numReplacements);
64+
65+
void onStringValueOf(Object param, @Nullable String result);
6466
}

0 commit comments

Comments
 (0)