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
+ }
}