From d3151b63d2ae30fa1745332309d5109d1e6c0cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Vidal=20Dom=C3=ADnguez?= <60353145+Mariovido@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:12:04 +0100 Subject: [PATCH] Add experimental taint propagation to the String replace, replaceFirst, replaceAll methods (#7741) --- .../iast/propagation/StringModuleImpl.java | 111 ++++++++++ .../java/com/datadog/iast/taint/Ranges.java | 42 +++- .../com/datadog/iast/util/RangeBuilder.java | 11 +- .../com/datadog/iast/util/StringUtils.java | 144 +++++++++++++ .../iast/propagation/StringModuleTest.groovy | 191 ++++++++++++++++-- .../com/datadog/iast/taint/RangesTest.groovy | 22 ++ .../datadog/iast/util/StringUtilsTest.groovy | 82 ++++++++ .../java/lang/StringCallSite.java | 17 ++ .../java/lang/StringExperimentalCallSite.java | 110 ++++++++++ .../java/lang/StringCallSiteTest.groovy | 22 +- .../StringExperimentalCallSiteTest.groovy | 95 +++++++++ .../test/java/foo/bar/TestStringSuite.java | 31 +++ .../datadog/trace/api/config/IastConfig.java | 2 + .../main/java/datadog/trace/api/Config.java | 8 +- .../trace/api/iast/IastEnabledChecks.java | 4 + .../api/iast/propagation/StringModule.java | 7 + .../trace/api/iast/telemetry/IastMetric.java | 3 +- .../api/iast/IastEnabledChecksTests.groovy | 16 ++ 18 files changed, 890 insertions(+), 28 deletions(-) create mode 100644 dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringExperimentalCallSite.java create mode 100644 dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringExperimentalCallSiteTest.groovy diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/propagation/StringModuleImpl.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/propagation/StringModuleImpl.java index 42deec0c489..774d0190303 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/propagation/StringModuleImpl.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/propagation/StringModuleImpl.java @@ -13,8 +13,10 @@ import com.datadog.iast.taint.TaintedObjects; import com.datadog.iast.util.RangeBuilder; import com.datadog.iast.util.Ranged; +import com.datadog.iast.util.StringUtils; import datadog.trace.api.iast.IastContext; import datadog.trace.api.iast.propagation.StringModule; +import de.thetaphi.forbiddenapis.SuppressForbidden; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.Arrays; import java.util.Deque; @@ -631,6 +633,115 @@ public void onIndent(@Nonnull String self, int indentation, @Nonnull String resu } } + @Override + @SuppressFBWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") + public void onStringReplace( + @Nonnull String self, char oldChar, char newChar, @Nonnull String result) { + if (self == result || !canBeTainted(result)) { + return; + } + final IastContext ctx = IastContext.Provider.get(); + if (ctx == null) { + return; + } + final TaintedObjects taintedObjects = ctx.getTaintedObjects(); + final TaintedObject taintedSelf = taintedObjects.get(self); + if (taintedSelf == null) { + return; + } + + final Range[] rangesSelf = taintedSelf.getRanges(); + if (rangesSelf.length == 0) { + return; + } + + taintedObjects.taint(result, rangesSelf); + } + + /** This method is used to make an {@code CallSite.Around} of the {@code String.replace} method */ + @Override + @SuppressFBWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") + public String onStringReplace( + @Nonnull String self, CharSequence oldCharSeq, CharSequence newCharSeq) { + final IastContext ctx = IastContext.Provider.get(); + if (ctx == null) { + return self.replace(oldCharSeq, newCharSeq); + } + final TaintedObjects taintedObjects = ctx.getTaintedObjects(); + final TaintedObject taintedSelf = taintedObjects.get(self); + Range[] rangesSelf = new Range[0]; + if (taintedSelf != null) { + rangesSelf = taintedSelf.getRanges(); + } + + final TaintedObject taintedInput = taintedObjects.get(newCharSeq); + Range[] rangesInput = null; + if (taintedInput != null) { + rangesInput = taintedInput.getRanges(); + } + + if (rangesSelf.length == 0 && rangesInput == null) { + return self.replace(oldCharSeq, newCharSeq); + } + + return StringUtils.replaceAndTaint( + taintedObjects, + self, + Pattern.compile((String) oldCharSeq), + (String) newCharSeq, + rangesSelf, + rangesInput, + Integer.MAX_VALUE); + } + + /** + * This method is used to make an {@code CallSite.Around} of the {@code String.replaceFirst} and + * {@code String.replaceAll} methods + */ + @Override + @SuppressForbidden + public String onStringReplace( + @Nonnull String self, String regex, String replacement, int numReplacements) { + final IastContext ctx = IastContext.Provider.get(); + if (ctx == null) { + if (numReplacements > 1) { + return self.replaceAll(regex, replacement); + } else { + return self.replaceFirst(regex, replacement); + } + } + + final TaintedObjects taintedObjects = ctx.getTaintedObjects(); + final TaintedObject taintedSelf = taintedObjects.get(self); + Range[] rangesSelf = new Range[0]; + if (taintedSelf != null) { + rangesSelf = taintedSelf.getRanges(); + } + + final TaintedObject taintedInput = taintedObjects.get(replacement); + Range[] rangesInput = null; + if (taintedInput != null) { + rangesInput = taintedInput.getRanges(); + } + + if (rangesSelf.length == 0 && rangesInput == null) { + if (numReplacements > 1) { + return self.replaceAll(regex, replacement); + } else { + return self.replaceFirst(regex, replacement); + } + } + + return StringUtils.replaceAndTaint( + taintedObjects, + self, + Pattern.compile(regex), + replacement, + rangesSelf, + rangesInput, + numReplacements); + } + /** * Adds the tainted ranges belonging to the current parameter added via placeholder taking care of * an optional tainted placeholder. diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/taint/Ranges.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/taint/Ranges.java index a84d7065386..a7b7c2df88d 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/taint/Ranges.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/taint/Ranges.java @@ -373,12 +373,11 @@ private static int updateRangesWithIndentation( if (range.getStart() + range.getLength() > end) { newLength -= lineOffset; } - newRanges[i] = new Range(newStart, newLength, range.getSource(), range.getMarks()); + newRanges[i] = copyWithPosition(range, newStart, newLength); } else if (range.getStart() + range.getLength() >= start) { final Range newRange = newRanges[i]; final int newLength = newRange.getLength() + indentation; - newRanges[i] = - new Range(newRange.getStart(), newLength, newRange.getSource(), newRange.getMarks()); + newRanges[i] = copyWithPosition(newRange, newRange.getStart(), newLength); } if (range.getStart() + range.getLength() - 1 <= end) { @@ -390,4 +389,41 @@ private static int updateRangesWithIndentation( return rangeStart; } + + /** + * Split the range in two taking into account the new length of the characters. + * + *

In case start and end are out of the range, it will return the range without splitting but + * taking into account the offset. In the case that the new length is less than or equal to 0, it + * will return an empty array. + * + * @param start is the start of the character sequence + * @param end is the end of the character sequence + * @param newLength is the new length of the character sequence + * @param range is the range to split + * @param offset is the offset to apply to the range + * @param diffLength is the difference between the new length and the old length + */ + public static Range[] splitRanges( + int start, int end, int newLength, Range range, int offset, int diffLength) { + start += offset; + end += offset; + int rangeStart = range.getStart() + offset; + int rangeEnd = rangeStart + range.getLength() + diffLength; + + int firstLength = start - rangeStart; + int secondLength = range.getLength() - firstLength - newLength + diffLength; + if (rangeStart > end || rangeEnd <= start) { + if (firstLength <= 0) { + return Ranges.EMPTY; + } + return new Range[] {copyWithPosition(range, rangeStart, firstLength)}; + } + + Range[] splittedRanges = new Range[2]; + splittedRanges[0] = copyWithPosition(range, rangeStart, firstLength); + splittedRanges[1] = copyWithPosition(range, rangeEnd - secondLength, secondLength); + + return splittedRanges; + } } diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/RangeBuilder.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/RangeBuilder.java index 83c925ae19f..52fdcae97db 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/RangeBuilder.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/RangeBuilder.java @@ -47,11 +47,15 @@ public RangeBuilder(final int maxSize, final int arrayChunkSize) { } public boolean add(final Range range) { + return add(range, 0); + } + + public boolean add(final Range range, final int offset) { if (size >= maxSize) { return false; } if (head == null) { - addNewEntry(new SingleEntry(range)); + addNewEntry(new SingleEntry(range.shift(offset))); } else { final ArrayEntry entry; if (tail instanceof ArrayEntry && tail.size() < arrayChunkSize) { @@ -60,7 +64,7 @@ public boolean add(final Range range) { entry = new ArrayEntry(arrayChunkSize); addNewEntry(entry); } - entry.add(range); + entry.add(range.shift(offset)); } size += 1; return true; @@ -77,6 +81,9 @@ public boolean add(final Range[] ranges, final int offset) { if (ranges.length == 0) { return true; } + if (ranges.length == 1) { + return add(ranges[0], offset); + } if (tail instanceof ArrayEntry && ranges.length <= (arrayChunkSize - tail.size())) { // compact intermediate ranges diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/StringUtils.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/StringUtils.java index 70b00ace442..86aa33ea576 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/StringUtils.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/util/StringUtils.java @@ -1,6 +1,12 @@ package com.datadog.iast.util; +import com.datadog.iast.model.Range; +import com.datadog.iast.taint.Ranges; +import com.datadog.iast.taint.TaintedObjects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public abstract class StringUtils { @@ -48,4 +54,142 @@ public static int leadingWhitespaces(@Nonnull final String value, int start, int } return whitespaces; } + + /** + * Returns the string replaced with the regex and the values tainted (if needed) + * + * @param taintedObjects the ctx object to save the range of the tainted values + * @param target the string to be replaced + * @param pattern the pattern to be replaced + * @param replacement the replacement string + * @param ranges the ranges of the string to be replaced + * @param rangesInput the ranges of the input string + * @param numOfReplacements the number of replacements to be made + */ + @Nonnull + public static String replaceAndTaint( + @Nonnull TaintedObjects taintedObjects, + @Nonnull final String target, + Pattern pattern, + String replacement, + Range[] ranges, + @Nullable Range[] rangesInput, + int numOfReplacements) { + if (numOfReplacements <= 0) { + return target; + } + Matcher matcher = pattern.matcher(target); + boolean result = matcher.find(); + if (result) { + int offset = 0; + RangeBuilder newRanges = new RangeBuilder(); + + int firstRange = 0; + int newLength = replacement.length(); + + boolean canAddRange = true; + StringBuffer sb = new StringBuffer(); + do { + int start = matcher.start(); + int end = matcher.end(); + int diffLength = newLength - (end - start); + + boolean rangesAdded = false; + while (firstRange < ranges.length && canAddRange) { + Range range = ranges[firstRange]; + int rangeStart = range.getStart(); + int rangeEnd = rangeStart + range.getLength(); + // If the replaced value is between one range + if (rangeStart <= start && rangeEnd >= end) { + Range[] splittedRanges = + Ranges.splitRanges(start, end, newLength, range, offset, diffLength); + + if (splittedRanges.length > 0 && splittedRanges[0].getLength() > 0) { + canAddRange = newRanges.add(splittedRanges[0]); + } + + if (rangesInput != null) { + canAddRange = newRanges.add(rangesInput, start + offset); + rangesAdded = true; + } + + if (splittedRanges.length > 1 && splittedRanges[1].getLength() > 0) { + canAddRange = newRanges.add(splittedRanges[1]); + } + + firstRange++; + break; + // If the replaced value starts in the range and not end there + } else if (rangeStart <= start && rangeEnd > start) { + Range[] splittedRanges = + Ranges.splitRanges(start, end, newLength, range, offset, diffLength); + + if (splittedRanges.length > 0 && splittedRanges[0].getLength() > 0) { + canAddRange = newRanges.add(splittedRanges[0]); + } + + if (rangesInput != null && !rangesAdded) { + canAddRange = newRanges.add(rangesInput, start + offset); + rangesAdded = true; + } + + // If the replaced value ends in the range + } else if (rangeEnd >= end) { + Range[] splittedRanges = + Ranges.splitRanges(start, end, newLength, range, offset, diffLength); + + if (rangesInput != null && !rangesAdded) { + canAddRange = newRanges.add(rangesInput, start + offset); + rangesAdded = true; + } + + if (splittedRanges.length > 1 && splittedRanges[1].getLength() > 0) { + canAddRange = newRanges.add(splittedRanges[1]); + } + + firstRange++; + break; + // Middle ranges + } else if (rangeStart >= start) { + firstRange++; + continue; + } else { + canAddRange = newRanges.add(range, rangeStart + offset); + } + + firstRange++; + } + + // In case there are no ranges + if (rangesInput != null && !rangesAdded && canAddRange) { + canAddRange = newRanges.add(rangesInput, start + offset); + } + + matcher.appendReplacement(sb, replacement); + + offset = diffLength; + numOfReplacements--; + if (numOfReplacements > 0) { + result = matcher.find(); + } + } while (result && numOfReplacements > 0); + + // In the case there is no tainted object + if (firstRange < ranges.length && canAddRange) { + for (int i = firstRange; i < ranges.length && canAddRange; i++) { + canAddRange = newRanges.add(ranges[i], offset); + } + } + + matcher.appendTail(sb); + String finalString = sb.toString(); + Range[] finalRanges = newRanges.toArray(); + if (finalRanges.length > 0) { + taintedObjects.taint(finalString, finalRanges); + } + return finalString; + } + + return target; + } } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy index 414df404a8e..617dc4aa499 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy @@ -1047,13 +1047,13 @@ class StringModuleTest extends IastModuleImplTestBase { result == expected where: - trailing | testString | expected - false | " ==> <== " | "" - false | " ==> <== " | "" - true | " ==> <== " | "" - false | "" | "" - false | "" | "" - true | "" | "" + trailing | testString | expected + false | " ==> <== " | "" + false | " ==> <== " | "" + true | " ==> <== " | "" + false | "" | "" + false | "" | "" + true | "" | "" } void 'test indent and make sure IastRequestContext is called'() { @@ -1081,19 +1081,170 @@ class StringModuleTest extends IastModuleImplTestBase { assert to == null } where: - indentation | testString | expected - 4 | "==>123<==\n12==>3<==" | " ==>123<==\n 12==>3<==" - 4 | "==>123<==\r\n12==>3<==" | " ==>123<==\n 12==>3<==" - 4 | "==>123\n1<==2==>3<==" | " ==>123\n 1<==2==>3<==" - 4 | "==>123\r\n1<==2==>3<==" | " ==>123\n 1<==2==>3<==" - 0 | "==>123<==\r\n==>123<==" | "==>123<==\n==>123<==" - 0 | "==>123\r\n<====>123<==" | "==>123\n<====>123<==" - 0 | "==>123<==\r==>123<==" | "==>123<==\n==>123<==" - 0 | "==>123\r<====>123<==" | "==>123\n<====>123<==" - -4 | " ==>123<==\n 12==>3<==" | "==>123<==\n12==>3<==" - -4 | " ==>123<==\r\n 12==>3<==" | "==>123<==\n12==>3<==" - -4 | " ==>123\n 1<==2==>3<==" | "==>123\n1<==2==>3<==" - -4 | " ==>123\r\n 1<==2==>3<==" | "==>123\n1<==2==>3<==" + indentation | testString | expected + 4 | "==>123<==\n12==>3<==" | " ==>123<==\n 12==>3<==" + 4 | "==>123<==\r\n12==>3<==" | " ==>123<==\n 12==>3<==" + 4 | "==>123\n1<==2==>3<==" | " ==>123\n 1<==2==>3<==" + 4 | "==>123\r\n1<==2==>3<==" | " ==>123\n 1<==2==>3<==" + 0 | "==>123<==\r\n==>123<==" | "==>123<==\n==>123<==" + 0 | "==>123\r\n<====>123<==" | "==>123\n<====>123<==" + 0 | "==>123<==\r==>123<==" | "==>123<==\n==>123<==" + 0 | "==>123\r<====>123<==" | "==>123\n<====>123<==" + -4 | " ==>123<==\n 12==>3<==" | "==>123<==\n12==>3<==" + -4 | " ==>123<==\r\n 12==>3<==" | "==>123<==\n12==>3<==" + -4 | " ==>123\n 1<==2==>3<==" | "==>123\n1<==2==>3<==" + -4 | " ==>123\r\n 1<==2==>3<==" | "==>123\n1<==2==>3<==" + } + + void 'test replace with a single char and make sure IastRequestContext is called'() { + given: + final taintedObjects = ctx.getTaintedObjects() + def self = addFromTaintFormat(taintedObjects, testString) + def result = self.replace(oldChar, newChar) + + when: + module.onStringReplace(self, oldChar as char, newChar as char, result) + def taintedObject = taintedObjects.get(result) + + then: + 1 * tracer.activeSpan() >> span + taintFormat(result, taintedObject.getRanges()) == expected + + where: + testString | oldChar | newChar | expected + "==>masquita<==" | 'a' | 'o' | "==>mosquito<==" + "==>___<==" | '_' | '-' | "==>---<==" + "==>my_input<==" | '_' | '-' | "==>my-input<==" + } + + void 'test replace with a char sequence (not tainted) and make sure IastRequestContext is called'() { + given: + final taintedObjects = ctx.getTaintedObjects() + def self = addFromTaintFormat(taintedObjects, testString) + + when: + def result = module.onStringReplace(self, oldCharSeq, newCharSeq) + def taintedObject = taintedObjects.get(result) + + then: + 1 * tracer.activeSpan() >> span + taintFormat(result, taintedObject.getRanges()) == expected + + where: + testString | oldCharSeq | newCharSeq | expected + "==>masquita<==" | 'as' | 'os' | "==>m<==os==>quita<==" + "==>masquita<==" | 'os' | 'as' | "==>masquita<==" + "==>m<==as==>qu<==i==>ta<==" | 'as' | 'os' | "==>m<==os==>qu<==i==>ta<==" + "==>my_input<==" | 'in' | 'out' | "==>my_<==out==>put<==" + "==>my_output<==" | 'out' | 'in' | "==>my_<==in==>put<==" + "==>my_input<==" | '_' | '-' | "==>my<==-==>input<==" + "==>my<==_==>input<==" | 'in' | 'out' | "==>my<==_out==>put<==" + "==>my_in<==p==>ut<==" | 'in' | 'out' | "==>my_<==outp==>ut<==" + "==>my_<==in==>put<==" | 'in' | 'out' | "==>my_<==out==>put<==" + "==>my_i<==n==>put<==" | 'in' | 'out' | "==>my_<==out==>put<==" + "==>my_<==i==>nput<==" | 'in' | 'out' | "==>my_<==out==>put<==" + "==>my_o<==u==>tput<==" | 'out' | 'in' | "==>my_<==in==>put<==" + "==>my_o<==u==>tput<====>my_o<==u==>tput<==" | 'out' | 'in' | "==>my_<==in==>put<====>my_<==in==>put<==" + "==>my_o<==u==>tp<==ut" | 'output' | 'input' | "==>my_<==input" + } + + void 'test replace with a char sequence (tainted) and make sure IastRequestContext is called'() { + given: + final taintedObjects = ctx.getTaintedObjects() + def self = addFromTaintFormat(taintedObjects, testString) + def inputTainted = addFromTaintFormat(taintedObjects, newCharSeq) + + when: + def result = module.onStringReplace(self, oldCharSeq, inputTainted) + def taintedObject = taintedObjects.get(result) + + then: + 1 * tracer.activeSpan() >> span + taintFormat(result, taintedObject.getRanges()) == expected + + where: + testString | oldCharSeq | newCharSeq | expected + "==>masquita<==" | 'as' | '==>os<==' | "==>m<====>os<====>quita<==" + "==>masquita<==" | 'os' | '==>as<==' | "==>masquita<==" + "masquita" | 'as' | '==>os<==' | "m==>os<==quita" + "==>m<==as==>qu<==i==>ta<==" | 'as' | '==>os<==' | "==>m<====>os<====>qu<==i==>ta<==" + "==>my_input<==" | 'in' | '==>out<==' | "==>my_<====>out<====>put<==" + "==>my_output<==" | 'out' | '==>in<==' | "==>my_<====>in<====>put<==" + "==>my_input<==" | '_' | '==>-<==' | "==>my<====>-<====>input<==" + "==>my<==_==>input<==" | 'in' | '==>out<==' | "==>my<==_==>out<====>put<==" + "==>my_in<==p==>ut<==" | 'in' | '==>out<==' | "==>my_<====>out<==p==>ut<==" + "==>my_<==in==>put<==" | 'in' | '==>out<==' | "==>my_<====>out<====>put<==" + "==>my_i<==n==>put<==" | 'in' | '==>out<==' | "==>my_<====>out<====>put<==" + "==>my_<==i==>nput<==" | 'in' | '==>out<==' | "==>my_<====>out<====>put<==" + "==>my_o<==u==>tput<==" | 'out' | '==>in<==' | "==>my_<====>in<====>put<==" + "==>my_o<==u==>tput<====>my_o<==u==>tput<==" | 'out' | '==>in<==' | "==>my_<====>in<====>put<====>my_<====>in<====>put<==" + "==>my_o<==u==>tp<==ut" | 'output' | '==>input<==' | "==>my_<====>input<==" + } + + void 'test replace with a regex and replacement (not tainted) and make sure IastRequestContext is called'() { + given: + final taintedObjects = ctx.getTaintedObjects() + def self = addFromTaintFormat(taintedObjects, testString) + + when: + def result = module.onStringReplace(self, regex, replacement, numReplacements) + def taintedObject = taintedObjects.get(result) + + then: + 1 * tracer.activeSpan() >> span + taintFormat(result, taintedObject.getRanges()) == expected + + where: + testString | regex | replacement | numReplacements | expected + "==>masquita<==" | 'as' | 'os' | Integer.MAX_VALUE | "==>m<==os==>quita<==" + "==>masquita<==" | 'os' | 'as' | Integer.MAX_VALUE | "==>masquita<==" + "==>m<==as==>qu<==i==>ta<==" | 'as' | 'os' | Integer.MAX_VALUE | "==>m<==os==>qu<==i==>ta<==" + "==>my_input<==" | 'in' | 'out' | Integer.MAX_VALUE | "==>my_<==out==>put<==" + "==>my_output<==" | 'out' | 'in' | Integer.MAX_VALUE | "==>my_<==in==>put<==" + "==>my_input<==" | '_' | '-' | Integer.MAX_VALUE | "==>my<==-==>input<==" + "==>my<==_==>input<==" | 'in' | 'out' | Integer.MAX_VALUE | "==>my<==_out==>put<==" + "==>my_in<==p==>ut<==" | 'in' | 'out' | Integer.MAX_VALUE | "==>my_<==outp==>ut<==" + "==>my_<==in==>put<==" | 'in' | 'out' | Integer.MAX_VALUE | "==>my_<==out==>put<==" + "==>my_i<==n==>put<==" | 'in' | 'out' | Integer.MAX_VALUE | "==>my_<==out==>put<==" + "==>my_<==i==>nput<==" | 'in' | 'out' | Integer.MAX_VALUE | "==>my_<==out==>put<==" + "==>my_o<==u==>tput<==" | 'out' | 'in' | Integer.MAX_VALUE | "==>my_<==in==>put<==" + "==>my_o<==u==>tput<====>my_o<==u==>tput<==" | 'out' | 'in' | Integer.MAX_VALUE | "==>my_<==in==>put<====>my_<==in==>put<==" + "==>my_o<==u==>tp<==ut" | 'output' | 'input' | Integer.MAX_VALUE | "==>my_<==input" + } + + void 'test replace with a regex and replacement (tainted) and make sure IastRequestContext is called'() { + given: + final taintedObjects = ctx.getTaintedObjects() + def self = addFromTaintFormat(taintedObjects, testString) + def inputTainted = addFromTaintFormat(taintedObjects, replacement) + + when: + def result = module.onStringReplace(self, regex, inputTainted, numReplacements) + def taintedObject = taintedObjects.get(result) + + then: + 1 * tracer.activeSpan() >> span + taintFormat(result, taintedObject.getRanges()) == expected + + where: + testString | regex | replacement | numReplacements | expected + "==>masquita<==" | 'as' | '==>os<==' | Integer.MAX_VALUE | "==>m<====>os<====>quita<==" + "==>masquita<==" | 'os' | '==>as<==' | Integer.MAX_VALUE | "==>masquita<==" + "masquita" | 'as' | '==>os<==' | Integer.MAX_VALUE | "m==>os<==quita" + "==>m<==as==>qu<==i==>ta<==" | 'as' | '==>os<==' | Integer.MAX_VALUE | "==>m<====>os<====>qu<==i==>ta<==" + "==>my_input<==" | 'in' | '==>out<==' | Integer.MAX_VALUE | "==>my_<====>out<====>put<==" + "==>my_output<==" | 'out' | '==>in<==' | Integer.MAX_VALUE | "==>my_<====>in<====>put<==" + "==>my_input<==" | '_' | '==>-<==' | Integer.MAX_VALUE | "==>my<====>-<====>input<==" + "==>my<==_==>input<==" | 'in' | '==>out<==' | Integer.MAX_VALUE | "==>my<==_==>out<====>put<==" + "==>my_in<==p==>ut<==" | 'in' | '==>out<==' | Integer.MAX_VALUE | "==>my_<====>out<==p==>ut<==" + "==>my_<==in==>put<==" | 'in' | '==>out<==' | Integer.MAX_VALUE | "==>my_<====>out<====>put<==" + "==>my_i<==n==>put<==" | 'in' | '==>out<==' | Integer.MAX_VALUE | "==>my_<====>out<====>put<==" + "==>my_<==i==>nput<==" | 'in' | '==>out<==' | Integer.MAX_VALUE | "==>my_<====>out<====>put<==" + "==>my_o<==u==>tput<==" | 'out' | '==>in<==' | Integer.MAX_VALUE | "==>my_<====>in<====>put<==" + "==>my_o<==u==>tput<====>my_o<==u==>tput<==" | 'out' | '==>in<==' | Integer.MAX_VALUE | "==>my_<====>in<====>put<====>my_<====>in<====>put<==" + "==>my_o<==u==>tp<==ut" | 'output' | '==>input<==' | Integer.MAX_VALUE | "==>my_<====>input<==" + "==>my_o<==u==>tput<====>my_o<==u==>tput<==" | 'out' | '==>in<==' | 1 | "==>my_<====>in<====>put<====>my_o<==u==>tput<==" + "==>my_o<==u==>tput<====>my_o<==u==>tput<==" | 'out' | '==>in<==' | 0 | "==>my_o<==u==>tput<====>my_o<==u==>tput<==" } private static Date date(final String pattern, final String value) { diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/RangesTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/RangesTest.groovy index d72c781f35e..7d39fa1cad6 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/RangesTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/taint/RangesTest.groovy @@ -355,6 +355,28 @@ class RangesTest extends DDSpecification { " 123\r\n 123" | -4 | [rangeWithSource(4, 10, (byte) 1, null, "123\r\n1"), rangeWithSource(15, 1, (byte) 2, null, "3")] | [rangeWithSource(0, 5, (byte) 1, null, "123\r\n1"), rangeWithSource(6, 1, (byte) 2, null, "3")] } + void 'test splitRanges method'() { + when: + final result = Ranges.splitRanges(start, end, newLength, range as Range, offset, diffLength) + + then: + final expectedArray = expected as Range[] + result == expectedArray + + where: + start | end | newLength | range | offset | diffLength | expected + 1 | 3 | 2 | range(0, 8) | 0 | 0 | [range(0, 1), range(3, 5)] + 1 | 3 | 2 | range(0, 8) | 2 | 0 | [range(2, 1), range(5, 5)] + 2 | 4 | 2 | range(1, 8) | -1 | 0 | [range(0, 1), range(3, 5)] + 1 | 3 | 3 | range(0, 8) | 0 | -1 | [range(0, 1), range(4, 3)] + 1 | 3 | 3 | range(0, 8) | 2 | -1 | [range(2, 1), range(6, 3)] + 2 | 3 | 2 | range(1, 8) | -1 | -1 | [range(0, 1), range(3, 4)] + 1 | 3 | 3 | range(0, 8) | 0 | 1 | [range(0, 1), range(4, 5)] + 1 | 3 | 3 | range(0, 8) | 2 | 1 | [range(2, 1), range(6, 5)] + 2 | 3 | 2 | range(1, 8) | -1 | 1 | [range(0, 1), range(3, 6)] + 8 | 10 | 2 | range(0, 8) | 0 | 0 | [range(0, 8)] + 1 | 3 | 2 | range(8, 8) | 0 | 0 | [] + } Range[] rangesFromSpec(List> spec) { def ranges = new Range[spec.size()] diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/util/StringUtilsTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/util/StringUtilsTest.groovy index 6109e366910..58e0aca69f3 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/util/StringUtilsTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/util/StringUtilsTest.groovy @@ -1,7 +1,13 @@ package com.datadog.iast.util +import com.datadog.iast.model.Range +import com.datadog.iast.model.Source +import com.datadog.iast.taint.TaintedObjects import spock.lang.Specification +import java.util.regex.Pattern +import static datadog.trace.api.iast.VulnerabilityMarks.NOT_MARKED + class StringUtilsTest extends Specification { void 'test ends with ignore case'() { @@ -43,4 +49,80 @@ class StringUtilsTest extends Specification { ' ab' | 0 | 3 | '' ' ab ' | 4 | 7 | '' } + + void 'test leadingWhitespaces method'() { + when: + final leadingWhitespaces = StringUtils.leadingWhitespaces(value, start, end) + + then: + leadingWhitespaces == expected + + where: + value | start | end | expected + " abc" | 0 | 3 | 3 + " abc" | 0 | 3 | 3 + " abc" | 0 | 3 | 2 + " abc " | 0 | 3 | 2 + } + + void 'test replaceAndTaint method'() { + given: + final taintedObjects = Mock(TaintedObjects) + + when: + final taintedString = StringUtils.replaceAndTaint(taintedObjects, target, pattern, replacement, ranges as Range[], rangesInput as Range[], numOfReplacement) + + then: + taintedString == expected + if (numOfReplacement > 0) { + 1 * taintedObjects.taint(expected, expectedRanges) + } else { + 0 * taintedObjects.taint(_, _) + } + + where: + target | pattern | replacement | ranges | rangesInput | numOfReplacement | expected | expectedRanges + "masquita" | Pattern.compile('as') | 'os' | [range(0, 8, null, "masquita")] | [range(0, 2, null, "os")] | Integer.MAX_VALUE | "mosquita" | [range(0, 1, null, "masquita"), range(1, 2, null, "os"), range(3, 5, null, "masquita")] + "masquita" | Pattern.compile('as') | 'os' | [range(0, 1, null, "m"), range(3, 2, null, "qu"), range(7, 2, null, "ta")] | [range(0, 2, null, "os")] | Integer.MAX_VALUE | "mosquita" | [range(0, 1, null, "m"), range(1, 2, null, "os"), range(3, 2, null, "qu"), range(7, 2, null, "ta")] + "my_outputmy_output" | Pattern.compile('out') | 'in' | [ + range(0, 4, null, "my_o"), + range(5, 4, null, "tput"), + range(9, 4, null, "my_o"), + range(14, 4, null, "tput") + ] | [range(0, 2, null, "in")] | Integer.MAX_VALUE | "my_inputmy_input" | [ + range(0, 3, null, "my_o"), + range(3, 2, null, "in"), + range(5, 3, null, "tput"), + range(8, 3, null, "my_o"), + range(11, 2, null, "in"), + range(13, 3, null, "tput") + ] + "my_outputmy_output" | Pattern.compile('out') | 'in' | [ + range(0, 4, null, "my_o"), + range(5, 4, null, "tput"), + range(9, 4, null, "my_o"), + range(14, 4, null, "tput") + ] | [range(0, 2, null, "in")] | 1 | "my_inputmy_output" | [ + range(0, 3, null, "my_o"), + range(3, 2, null, "in"), + range(5, 3, null, "tput"), + range(8, 4, null, "my_o"), + range(13, 4, null, "tput") + ] + "my_outputmy_output" | Pattern.compile('out') | 'in' | [ + range(0, 4, null, "my_o"), + range(5, 4, null, "tput"), + range(9, 4, null, "my_o"), + range(14, 4, null, "tput") + ] | [range(0, 2, null, "in")] | 0 | "my_outputmy_output" | [ + range(0, 4, null, "my_o"), + range(5, 4, null, "tput"), + range(9, 4, null, "my_o"), + range(14, 4, null, "tput") + ] + } + + Range range(final int start, final int length, final String name = 'name', final String value = 'value') { + return new Range(start, length, new Source((byte) 1, name, value), NOT_MARKED) + } } diff --git a/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringCallSite.java b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringCallSite.java index 9c32298bdec..9d845256077 100644 --- a/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringCallSite.java +++ b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringCallSite.java @@ -281,4 +281,21 @@ public static String[] afterSplitWithLimit( } return result; } + + @CallSite.After("java.lang.String java.lang.String.replace(char, char)") + public static String afterReplaceChar( + @CallSite.This @Nonnull final String self, + @CallSite.Argument(0) final char oldChar, + @CallSite.Argument(1) final char newChar, + @CallSite.Return @Nonnull final String result) { + final StringModule module = InstrumentationBridge.STRING; + if (module != null) { + try { + module.onStringReplace(self, oldChar, newChar, result); + } catch (final Throwable e) { + module.onUnexpectedException("afterReplaceChar threw", e); + } + } + return result; + } } diff --git a/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringExperimentalCallSite.java b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringExperimentalCallSite.java new file mode 100644 index 00000000000..762b6eaa84c --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/StringExperimentalCallSite.java @@ -0,0 +1,110 @@ +package datadog.trace.instrumentation.java.lang; + +import static datadog.trace.api.telemetry.LogCollector.SEND_TELEMETRY; + +import datadog.trace.agent.tooling.csi.CallSite; +import datadog.trace.api.iast.IastCallSites; +import datadog.trace.api.iast.InstrumentationBridge; +import datadog.trace.api.iast.Propagation; +import datadog.trace.api.iast.propagation.StringModule; +import datadog.trace.api.iast.telemetry.IastMetric; +import datadog.trace.api.iast.telemetry.IastMetricCollector; +import de.thetaphi.forbiddenapis.SuppressForbidden; +import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Propagation +@CallSite( + spi = IastCallSites.class, + enabled = {"datadog.trace.api.iast.IastEnabledChecks", "isExperimentalPropagationEnabled"}) +public class StringExperimentalCallSite { + + private static final Logger LOGGER = LoggerFactory.getLogger(StringExperimentalCallSite.class); + + @CallSite.After( + "java.lang.String java.lang.String.replace(java.lang.CharSequence, java.lang.CharSequence)") + public static String afterReplaceCharSeq( + @CallSite.This @Nonnull final String self, + @CallSite.Argument(0) final CharSequence oldCharSeq, + @CallSite.Argument(1) final CharSequence newCharSeq, + @CallSite.Return @Nonnull final String result) { + String newReplaced = ""; + final StringModule module = InstrumentationBridge.STRING; + if (module != null) { + try { + newReplaced = module.onStringReplace(self, oldCharSeq, newCharSeq); + } catch (final Throwable e) { + module.onUnexpectedException("afterReplaceCharSeq threw", e); + } + } + if (!result.equals(newReplaced)) { + LOGGER.debug( + SEND_TELEMETRY, + "afterReplaceCharSeq failed due to a different result between original replace and new replace, originalLength: {}, newLength: {}", + result.length(), + newReplaced != null ? newReplaced.length() : 0); + } + + IastMetricCollector.add(IastMetric.EXPERIMENTAL_PROPAGATION, 1); + return result; + } + + @CallSite.After( + "java.lang.String java.lang.String.replaceAll(java.lang.String, java.lang.String)") + @SuppressForbidden + public static String afterReplaceAll( + @CallSite.This final String self, + @CallSite.Argument(0) final String regex, + @CallSite.Argument(1) final String replacement, + @CallSite.Return @Nonnull final String result) { + String newReplaced = ""; + final StringModule module = InstrumentationBridge.STRING; + if (module != null) { + try { + newReplaced = module.onStringReplace(self, regex, replacement, Integer.MAX_VALUE); + } catch (final Throwable e) { + module.onUnexpectedException("afterReplaceAll threw", e); + } + } + if (!result.equals(newReplaced)) { + LOGGER.debug( + SEND_TELEMETRY, + "afterReplaceAll failed due to a different result between original replace and new replace, originalLength: {}, newLength: {}", + result.length(), + newReplaced != null ? newReplaced.length() : 0); + } + + IastMetricCollector.add(IastMetric.EXPERIMENTAL_PROPAGATION, 1); + return result; + } + + @CallSite.After( + "java.lang.String java.lang.String.replaceFirst(java.lang.String, java.lang.String)") + @SuppressForbidden + public static String afterReplaceFirst( + @CallSite.This final String self, + @CallSite.Argument(0) final String regex, + @CallSite.Argument(1) final String replacement, + @CallSite.Return @Nonnull final String result) { + String newReplaced = ""; + final StringModule module = InstrumentationBridge.STRING; + if (module != null) { + try { + newReplaced = module.onStringReplace(self, regex, replacement, 1); + } catch (final Throwable e) { + module.onUnexpectedException("afterReplaceFirst threw", e); + } + } + if (!result.equals(newReplaced)) { + LOGGER.debug( + SEND_TELEMETRY, + "afterReplaceFirst failed due to a different result between original replace and new replace, originalLength: {}, newLength: {}", + result.length(), + newReplaced != null ? newReplaced.length() : 0); + } + + IastMetricCollector.add(IastMetric.EXPERIMENTAL_PROPAGATION, 1); + return result; + } +} diff --git a/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringCallSiteTest.groovy b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringCallSiteTest.groovy index 9268ce2d140..24c4f7b1b26 100644 --- a/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringCallSiteTest.groovy +++ b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringCallSiteTest.groovy @@ -6,12 +6,14 @@ import datadog.trace.api.iast.propagation.StringModule import foo.bar.TestStringSuite import groovy.transform.CompileDynamic +import static datadog.trace.api.config.IastConfig.IAST_ENABLED + @CompileDynamic class StringCallSiteTest extends AgentTestRunner { @Override protected void configurePreAgent() { - injectSysConfig("dd.iast.enabled", "true") + injectSysConfig(IAST_ENABLED, "true") } def 'test string concat call site'() { @@ -239,4 +241,22 @@ class StringCallSiteTest extends AgentTestRunner { ['test the test', ' '] | ['test', 'the', 'test'] as String[] ['test the test', ' ', 0] | ['test', 'the', 'test'] as String[] } + + void 'test string replace char'() { + given: + final module = Mock(StringModule) + InstrumentationBridge.registerIastModule(module) + + when: + def result = TestStringSuite.replace(input, oldChar as char, newChar as char) + + then: + result == expected + 1 * module.onStringReplace(input, oldChar, newChar, expected) + + where: + input | oldChar | newChar | expected + "test" | 't' | 'T' | "TesT" + "test" | 'e' | 'E' | "tEst" + } } diff --git a/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringExperimentalCallSiteTest.groovy b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringExperimentalCallSiteTest.groovy new file mode 100644 index 00000000000..cc1a556d726 --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/StringExperimentalCallSiteTest.groovy @@ -0,0 +1,95 @@ +package datadog.trace.instrumentation.java.lang + +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.iast.InstrumentationBridge +import datadog.trace.api.iast.propagation.StringModule +import foo.bar.TestStringSuite + +import static datadog.trace.api.config.IastConfig.IAST_ENABLED +import static datadog.trace.api.config.IastConfig.IAST_EXPERIMENTAL_PROPAGATION_ENABLED + +class StringExperimentalCallSiteTest extends AgentTestRunner { + + @Override + protected void configurePreAgent() { + injectSysConfig(IAST_ENABLED, "true") + injectSysConfig(IAST_EXPERIMENTAL_PROPAGATION_ENABLED, "true") + } + + void 'test string replace char sequence'() { + given: + final module = Mock(StringModule) + InstrumentationBridge.registerIastModule(module) + + when: + TestStringSuite.replace(input, oldCharSeq, newCharSeq) + + then: + 1 * module.onStringReplace(input, oldCharSeq, newCharSeq) + + where: + input | oldCharSeq | newCharSeq + "test" | 'te' | 'TE' + "test" | 'es' | 'ES' + } + + void 'test string replace char sequence (throw error)'() { + given: + final module = Mock(StringModule) + InstrumentationBridge.registerIastModule(module) + module.onStringReplace(_ as String, _ as CharSequence, _ as CharSequence) >> { throw new Error("test error") } + + when: + def result = TestStringSuite.replace(input, oldCharSeq, newCharSeq) + + then: + result == expected + 1 * module.onUnexpectedException("afterReplaceCharSeq threw", _ as Error) + + where: + input | oldCharSeq | newCharSeq | expected + "test" | 'te' | 'TE' | 'TEst' + "test" | 'es' | 'ES' | 'tESt' + } + + void 'test string replace all and replace first with regex'() { + given: + final module = Mock(StringModule) + InstrumentationBridge.registerIastModule(module) + + when: + TestStringSuite."$method"(input, regex, replacement) + + then: + 1 * module.onStringReplace(input, regex, replacement, numReplacements) + + where: + method | input | regex | replacement | numReplacements + "replaceAll" | "test" | 'te' | 'TE' | Integer.MAX_VALUE + "replaceAll" | "test" | 'es' | 'ES' | Integer.MAX_VALUE + "replaceFirst" | "test" | 'te' | 'TE' | 1 + "replaceFirst" | "test" | 'es' | 'ES' | 1 + } + + void 'test string replace all and replace first with regex (throw error)'() { + given: + final module = Mock(StringModule) + InstrumentationBridge.registerIastModule(module) + module.onStringReplace(_ as String, _ as String, _ as String, numReplacements) >> { throw new Error("test error") } + final textError = "afterR" + method.substring(1) + " threw" + + when: + def result = TestStringSuite."$method"(input, regex, replacement) + + then: + result == expected + 1 * module.onUnexpectedException(textError, _ as Error) + + where: + method | input | regex | replacement | numReplacements | expected + "replaceAll" | "test" | 'te' | 'TE' | Integer.MAX_VALUE | 'TEst' + "replaceAll" | "test" | 'es' | 'ES' | Integer.MAX_VALUE | 'tESt' + "replaceFirst" | "test" | 'te' | 'TE' | 1 | 'TEst' + "replaceFirst" | "test" | 'es' | 'ES' | 1 | 'tESt' + } +} diff --git a/dd-java-agent/instrumentation/java-lang/src/test/java/foo/bar/TestStringSuite.java b/dd-java-agent/instrumentation/java-lang/src/test/java/foo/bar/TestStringSuite.java index 266235d4a67..2e1c00f4f50 100644 --- a/dd-java-agent/instrumentation/java-lang/src/test/java/foo/bar/TestStringSuite.java +++ b/dd-java-agent/instrumentation/java-lang/src/test/java/foo/bar/TestStringSuite.java @@ -194,4 +194,35 @@ public static String[] split(final String string, final String regex, final int LOGGER.debug("After split {}", result); return result; } + + public static String replace(final String string, final char oldChar, final char newChar) { + LOGGER.debug("Before replace {} {} {}", string, oldChar, newChar); + String result = string.replace(oldChar, newChar); + LOGGER.debug("After replace {}", result); + return result; + } + + public static String replace( + final String string, final CharSequence oldCharSeq, final CharSequence newCharSeq) { + LOGGER.debug("Before replace {} {} {}", string, oldCharSeq, newCharSeq); + String result = string.replace(oldCharSeq, newCharSeq); + LOGGER.debug("After replace {}", result); + return result; + } + + public static String replaceAll( + final String string, final String regex, final String replacement) { + LOGGER.debug("Before replace all {} {} {}", string, regex, replacement); + String result = string.replaceAll(regex, replacement); + LOGGER.debug("After replace all {}", result); + return result; + } + + public static String replaceFirst( + final String string, final String regex, final String replacement) { + LOGGER.debug("Before replace first {} {} {}", string, regex, replacement); + String result = string.replaceFirst(regex, replacement); + LOGGER.debug("After replace first {}", result); + return result; + } } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/IastConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/IastConfig.java index 3c1c9441ed5..73f0b93a4c9 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/IastConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/IastConfig.java @@ -25,6 +25,8 @@ public final class IastConfig { public static final String IAST_ANONYMOUS_CLASSES_ENABLED = "iast.anonymous-classes.enabled"; public static final String IAST_SOURCE_MAPPING_ENABLED = "iast.source-mapping.enabled"; public static final String IAST_SOURCE_MAPPING_MAX_SIZE = "iast.source-mapping.max-size"; + public static final String IAST_EXPERIMENTAL_PROPAGATION_ENABLED = + "iast.experimental.propagation.enabled"; public static final String IAST_STACK_TRACE_ENABLED = "iast.stacktrace.enabled"; diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 2fc71685875..5e757a807c7 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -304,6 +304,7 @@ public static String getHostName() { private final boolean iastSourceMappingEnabled; private final int iastSourceMappingMaxSize; private final boolean iastStackTraceEnabled; + private final boolean iastExperimentalPropagationEnabled; private final boolean ciVisibilityTraceSanitationEnabled; private final boolean ciVisibilityAgentlessEnabled; @@ -1309,9 +1310,10 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) IAST_ANONYMOUS_CLASSES_ENABLED, DEFAULT_IAST_ANONYMOUS_CLASSES_ENABLED); iastSourceMappingEnabled = configProvider.getBoolean(IAST_SOURCE_MAPPING_ENABLED, false); iastSourceMappingMaxSize = configProvider.getInteger(IAST_SOURCE_MAPPING_MAX_SIZE, 1000); - iastStackTraceEnabled = configProvider.getBoolean(IAST_STACK_TRACE_ENABLED, DEFAULT_IAST_STACK_TRACE_ENABLED); + iastExperimentalPropagationEnabled = + configProvider.getBoolean(IAST_EXPERIMENTAL_PROPAGATION_ENABLED, false); ciVisibilityTraceSanitationEnabled = configProvider.getBoolean(CIVISIBILITY_TRACE_SANITATION_ENABLED, true); @@ -2592,6 +2594,10 @@ public boolean isIastStackTraceEnabled() { return iastStackTraceEnabled; } + public boolean isIastExperimentalPropagationEnabled() { + return iastExperimentalPropagationEnabled; + } + public boolean isCiVisibilityEnabled() { return instrumenterConfig.isCiVisibilityEnabled(); } diff --git a/internal-api/src/main/java/datadog/trace/api/iast/IastEnabledChecks.java b/internal-api/src/main/java/datadog/trace/api/iast/IastEnabledChecks.java index 2f05a7a763f..c955769841d 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/IastEnabledChecks.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/IastEnabledChecks.java @@ -26,4 +26,8 @@ public static boolean isMajorJavaVersionAtLeast(final String version) { public static boolean isFullDetection() { return Config.get().getIastDetectionMode() == IastDetectionMode.FULL; } + + public static boolean isExperimentalPropagationEnabled() { + return Config.get().isIastExperimentalPropagationEnabled(); + } } diff --git a/internal-api/src/main/java/datadog/trace/api/iast/propagation/StringModule.java b/internal-api/src/main/java/datadog/trace/api/iast/propagation/StringModule.java index 225c192d0f2..c282b8a93de 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/propagation/StringModule.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/propagation/StringModule.java @@ -54,4 +54,11 @@ void onStringFormat( void onStringStrip(@Nonnull String self, @Nonnull String result, boolean trailing); void onIndent(@Nonnull String self, int indentation, @Nonnull String result); + + void onStringReplace(@Nonnull String self, char oldChar, char newChar, @Nonnull String result); + + String onStringReplace(@Nonnull String self, CharSequence oldCharSeq, CharSequence newCharSeq); + + String onStringReplace( + @Nonnull String self, String regex, String replacement, int numReplacements); } diff --git a/internal-api/src/main/java/datadog/trace/api/iast/telemetry/IastMetric.java b/internal-api/src/main/java/datadog/trace/api/iast/telemetry/IastMetric.java index 6e2942b3a03..cfe18789e65 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/telemetry/IastMetric.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/telemetry/IastMetric.java @@ -21,7 +21,8 @@ public enum IastMetric { TAINTED_FLAT_MODE("tainted.flat.mode", false, Scope.GLOBAL, Verbosity.INFORMATION), JSON_TAG_SIZE_EXCEED("json.tag.size.exceeded", true, Scope.GLOBAL, Verbosity.INFORMATION), SOURCE_MAPPING_LIMIT_REACHED( - "source.mapping.limit.reached", true, Scope.GLOBAL, Verbosity.INFORMATION); + "source.mapping.limit.reached", true, Scope.GLOBAL, Verbosity.INFORMATION), + EXPERIMENTAL_PROPAGATION("experimental.propagation", false, Scope.GLOBAL, Verbosity.INFORMATION); private static final int COUNT; diff --git a/internal-api/src/test/groovy/datadog/trace/api/iast/IastEnabledChecksTests.groovy b/internal-api/src/test/groovy/datadog/trace/api/iast/IastEnabledChecksTests.groovy index 599866eea9e..aab4b750459 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/iast/IastEnabledChecksTests.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/iast/IastEnabledChecksTests.groovy @@ -36,4 +36,20 @@ class IastEnabledChecksTests extends DDSpecification { IastDetectionMode.FULL | true IastDetectionMode.DEFAULT | false } + + void 'test experimental propagation'() { + setup: + injectSysConfig(IastConfig.IAST_EXPERIMENTAL_PROPAGATION_ENABLED, value) + + when: + final isExperimentalPropagationEnabled = IastEnabledChecks.isExperimentalPropagationEnabled() + + then: + isExperimentalPropagationEnabled == expected + + where: + value | expected + "true" | true + "false" | false + } }