Skip to content

Commit

Permalink
Add experimental taint propagation to the String replace, replaceFirs…
Browse files Browse the repository at this point in the history
…t, replaceAll methods (#7741)
  • Loading branch information
Mariovido authored Nov 8, 2024
1 parent 8b31030 commit d3151b6
Show file tree
Hide file tree
Showing 18 changed files with 890 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -390,4 +389,41 @@ private static int updateRangesWithIndentation(

return rangeStart;
}

/**
* Split the range in two taking into account the new length of the characters.
*
* <p>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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -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;
}
}
Loading

0 comments on commit d3151b6

Please sign in to comment.