diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java index 04ebec93407..21531fb2a6e 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/model/VulnerabilityType.java @@ -15,98 +15,153 @@ import static datadog.trace.api.iast.VulnerabilityMarks.XPATH_INJECTION_MARK; import static datadog.trace.api.iast.VulnerabilityMarks.XSS_MARK; +import datadog.trace.api.iast.SourceTypes; import datadog.trace.api.iast.VulnerabilityTypes; import java.io.File; +import java.util.BitSet; import java.util.function.BiFunction; import java.util.zip.CRC32; import javax.annotation.Nonnull; public interface VulnerabilityType { - VulnerabilityType WEAK_CIPHER = type(VulnerabilityTypes.WEAK_CIPHER).build(); - VulnerabilityType WEAK_HASH = type(VulnerabilityTypes.WEAK_HASH).build(); + BitSet DB_EXCLUDED = new BitSet(SourceTypes.SQL_TABLE); + + VulnerabilityType WEAK_CIPHER = + type(VulnerabilityTypes.WEAK_CIPHER).excludedSources(DB_EXCLUDED).build(); + VulnerabilityType WEAK_HASH = + type(VulnerabilityTypes.WEAK_HASH).excludedSources(DB_EXCLUDED).build(); VulnerabilityType INSECURE_COOKIE = - type(VulnerabilityTypes.INSECURE_COOKIE).hash(VulnerabilityType::evidenceHash).build(); + type(VulnerabilityTypes.INSECURE_COOKIE) + .hash(VulnerabilityType::evidenceHash) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType NO_HTTPONLY_COOKIE = - type(VulnerabilityTypes.NO_HTTPONLY_COOKIE).hash(VulnerabilityType::evidenceHash).build(); + type(VulnerabilityTypes.NO_HTTPONLY_COOKIE) + .hash(VulnerabilityType::evidenceHash) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType HSTS_HEADER_MISSING = - type(VulnerabilityTypes.HSTS_HEADER_MISSING).hash(VulnerabilityType::serviceHash).build(); + type(VulnerabilityTypes.HSTS_HEADER_MISSING) + .hash(VulnerabilityType::serviceHash) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType XCONTENTTYPE_HEADER_MISSING = type(VulnerabilityTypes.XCONTENTTYPE_HEADER_MISSING) .hash(VulnerabilityType::serviceHash) + .excludedSources(DB_EXCLUDED) .build(); VulnerabilityType NO_SAMESITE_COOKIE = - type(VulnerabilityTypes.NO_SAMESITE_COOKIE).hash(VulnerabilityType::evidenceHash).build(); + type(VulnerabilityTypes.NO_SAMESITE_COOKIE) + .hash(VulnerabilityType::evidenceHash) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType SQL_INJECTION = type(VulnerabilityTypes.SQL_INJECTION).mark(SQL_INJECTION_MARK).build(); VulnerabilityType COMMAND_INJECTION = - type(VulnerabilityTypes.COMMAND_INJECTION).mark(COMMAND_INJECTION_MARK).build(); + type(VulnerabilityTypes.COMMAND_INJECTION) + .mark(COMMAND_INJECTION_MARK) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType PATH_TRAVERSAL = type(VulnerabilityTypes.PATH_TRAVERSAL) .separator(File.separatorChar) .mark(PATH_TRAVERSAL_MARK) + .excludedSources(DB_EXCLUDED) .build(); VulnerabilityType LDAP_INJECTION = - type(VulnerabilityTypes.LDAP_INJECTION).mark(LDAP_INJECTION_MARK).build(); - VulnerabilityType SSRF = type(VulnerabilityTypes.SSRF).mark(SSRF_MARK).build(); + type(VulnerabilityTypes.LDAP_INJECTION) + .mark(LDAP_INJECTION_MARK) + .excludedSources(DB_EXCLUDED) + .build(); + VulnerabilityType SSRF = + type(VulnerabilityTypes.SSRF).mark(SSRF_MARK).excludedSources(DB_EXCLUDED).build(); VulnerabilityType UNVALIDATED_REDIRECT = - type(VulnerabilityTypes.UNVALIDATED_REDIRECT).mark(UNVALIDATED_REDIRECT_MARK).build(); - VulnerabilityType WEAK_RANDOMNESS = type(VulnerabilityTypes.WEAK_RANDOMNESS).build(); + type(VulnerabilityTypes.UNVALIDATED_REDIRECT) + .mark(UNVALIDATED_REDIRECT_MARK) + .excludedSources(DB_EXCLUDED) + .build(); + VulnerabilityType WEAK_RANDOMNESS = + type(VulnerabilityTypes.WEAK_RANDOMNESS).excludedSources(DB_EXCLUDED).build(); VulnerabilityType XPATH_INJECTION = - type(VulnerabilityTypes.XPATH_INJECTION).mark(XPATH_INJECTION_MARK).build(); + type(VulnerabilityTypes.XPATH_INJECTION) + .mark(XPATH_INJECTION_MARK) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType TRUST_BOUNDARY_VIOLATION = - type(VulnerabilityTypes.TRUST_BOUNDARY_VIOLATION).mark(TRUST_BOUNDARY_VIOLATION_MARK).build(); + type(VulnerabilityTypes.TRUST_BOUNDARY_VIOLATION) + .mark(TRUST_BOUNDARY_VIOLATION_MARK) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType XSS = type(VulnerabilityTypes.XSS).mark(XSS_MARK).build(); VulnerabilityType HEADER_INJECTION = - type(VulnerabilityTypes.HEADER_INJECTION).mark(HEADER_INJECTION_MARK).build(); + type(VulnerabilityTypes.HEADER_INJECTION) + .mark(HEADER_INJECTION_MARK) + .excludedSources(DB_EXCLUDED) + .build(); - VulnerabilityType STACKTRACE_LEAK = type(VulnerabilityTypes.STACKTRACE_LEAK).build(); + VulnerabilityType STACKTRACE_LEAK = + type(VulnerabilityTypes.STACKTRACE_LEAK).excludedSources(DB_EXCLUDED).build(); - VulnerabilityType VERB_TAMPERING = type(VulnerabilityTypes.VERB_TAMPERING).build(); + VulnerabilityType VERB_TAMPERING = + type(VulnerabilityTypes.VERB_TAMPERING).excludedSources(DB_EXCLUDED).build(); VulnerabilityType ADMIN_CONSOLE_ACTIVE = type(VulnerabilityTypes.ADMIN_CONSOLE_ACTIVE) .deduplicable(false) .hash(VulnerabilityType::serviceHash) + .excludedSources(DB_EXCLUDED) .build(); VulnerabilityType DEFAULT_HTML_ESCAPE_INVALID = - type(VulnerabilityTypes.DEFAULT_HTML_ESCAPE_INVALID).build(); + type(VulnerabilityTypes.DEFAULT_HTML_ESCAPE_INVALID).excludedSources(DB_EXCLUDED).build(); - VulnerabilityType SESSION_TIMEOUT = type(VulnerabilityTypes.SESSION_TIMEOUT).build(); + VulnerabilityType SESSION_TIMEOUT = + type(VulnerabilityTypes.SESSION_TIMEOUT).excludedSources(DB_EXCLUDED).build(); VulnerabilityType DIRECTORY_LISTING_LEAK = - type(VulnerabilityTypes.DIRECTORY_LISTING_LEAK).build(); - VulnerabilityType INSECURE_JSP_LAYOUT = type(VulnerabilityTypes.INSECURE_JSP_LAYOUT).build(); + type(VulnerabilityTypes.DIRECTORY_LISTING_LEAK).excludedSources(DB_EXCLUDED).build(); + VulnerabilityType INSECURE_JSP_LAYOUT = + type(VulnerabilityTypes.INSECURE_JSP_LAYOUT).excludedSources(DB_EXCLUDED).build(); - VulnerabilityType HARDCODED_SECRET = type(VulnerabilityTypes.HARDCODED_SECRET).build(); + VulnerabilityType HARDCODED_SECRET = + type(VulnerabilityTypes.HARDCODED_SECRET).excludedSources(DB_EXCLUDED).build(); VulnerabilityType INSECURE_AUTH_PROTOCOL = - type(VulnerabilityTypes.INSECURE_AUTH_PROTOCOL).hash(VulnerabilityType::evidenceHash).build(); + type(VulnerabilityTypes.INSECURE_AUTH_PROTOCOL) + .hash(VulnerabilityType::evidenceHash) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType REFLECTION_INJECTION = - type(VulnerabilityTypes.REFLECTION_INJECTION).mark(REFLECTION_INJECTION_MARK).build(); + type(VulnerabilityTypes.REFLECTION_INJECTION) + .mark(REFLECTION_INJECTION_MARK) + .excludedSources(DB_EXCLUDED) + .build(); VulnerabilityType SESSION_REWRITING = type(VulnerabilityTypes.SESSION_REWRITING) .deduplicable(false) .hash(VulnerabilityType::serviceHash) + .excludedSources(DB_EXCLUDED) .build(); VulnerabilityType DEFAULT_APP_DEPLOYED = type(VulnerabilityTypes.DEFAULT_APP_DEPLOYED) .deduplicable(false) .hash(VulnerabilityType::serviceHash) + .excludedSources(DB_EXCLUDED) .build(); VulnerabilityType UNTRUSTED_DESERIALIZATION = type(VulnerabilityTypes.UNTRUSTED_DESERIALIZATION) .mark(UNTRUSTED_DESERIALIZATION_MARK) + .excludedSources(DB_EXCLUDED) .build(); /* All vulnerability types that have a mark. Should be updated if new vulnerabilityType with mark is added */ @@ -139,6 +194,8 @@ public interface VulnerabilityType { byte type(); + BitSet excludedSources(); + static Builder type(final byte type) { return new Builder(type); } @@ -153,6 +210,8 @@ class VulnerabilityTypeImpl implements VulnerabilityType { private final boolean deduplicable; + private final BitSet excludedSources; + private final BiFunction hash; public VulnerabilityTypeImpl( @@ -160,11 +219,13 @@ public VulnerabilityTypeImpl( final char separator, final int mark, final boolean deduplicable, + final BitSet excludedSources, final BiFunction hash) { this.type = type; this.separator = separator; this.mark = mark; this.deduplicable = deduplicable; + this.excludedSources = excludedSources; this.hash = hash; } @@ -198,6 +259,11 @@ public byte type() { return type; } + @Override + public BitSet excludedSources() { + return excludedSources; + } + /** Useful for troubleshooting issues when vulns are serialized without moshi */ public String getName() { return name(); @@ -209,6 +275,7 @@ class Builder { private char separator = ' '; private int mark = NOT_MARKED; private boolean deduplicable = true; + private BitSet excludedSources = new BitSet(); private BiFunction hash = VulnerabilityType::fileAndLineHash; @@ -231,13 +298,18 @@ public Builder deduplicable(final boolean deduplicable) { return this; } + public Builder excludedSources(final BitSet excludedSources) { + this.excludedSources = excludedSources; + return this; + } + public Builder hash(final BiFunction hash) { this.hash = hash; return this; } public VulnerabilityType build() { - return new VulnerabilityTypeImpl(type, separator, mark, deduplicable, hash); + return new VulnerabilityTypeImpl(type, separator, mark, deduplicable, excludedSources, hash); } } diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/SinkModuleBase.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/SinkModuleBase.java index 45938cc72c4..fb694a96d77 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/SinkModuleBase.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/SinkModuleBase.java @@ -148,9 +148,21 @@ protected final Evidence checkInjection( return null; } + // filter excluded ranges + final Range[] filteredRanges; + if (!type.excludedSources().isEmpty()) { + filteredRanges = Ranges.excludeRangesBySource(valueRanges, type.excludedSources()); + } else { + filteredRanges = valueRanges; + } + + if (filteredRanges == null || filteredRanges.length == 0) { + return null; + } + final StringBuilder evidence = new StringBuilder(); final RangeBuilder ranges = new RangeBuilder(); - addToEvidence(type, evidence, ranges, value, valueRanges, evidenceBuilder); + addToEvidence(type, evidence, ranges, value, filteredRanges, evidenceBuilder); // check if finally we have an injection if (ranges.isEmpty()) { 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 a7b7c2df88d..91bfe870401 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 @@ -10,6 +10,7 @@ import com.datadog.iast.util.Ranged; import com.datadog.iast.util.StringUtils; import datadog.trace.api.iast.SourceTypes; +import java.util.BitSet; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -426,4 +427,22 @@ public static Range[] splitRanges( return splittedRanges; } + + /** + * Remove the ranges that have the same origin as the input source. + * + * @param ranges the ranges to filter + * @param source the byte value of the source to exclude (see {@link SourceTypes}) + */ + public static Range[] excludeRangesBySource(Range[] ranges, BitSet source) { + RangeBuilder newRanges = new RangeBuilder(ranges.length); + + for (Range range : ranges) { + if (!source.get(range.getSource().getOrigin())) { + newRanges.add(range); + } + } + + return newRanges.toArray(); + } } 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 7d39fa1cad6..42f6a97bad2 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 @@ -12,6 +12,8 @@ import static com.datadog.iast.util.HttpHeader.LOCATION import static com.datadog.iast.util.HttpHeader.REFERER import static datadog.trace.api.iast.SourceTypes.GRPC_BODY import static datadog.trace.api.iast.SourceTypes.REQUEST_HEADER_VALUE +import static datadog.trace.api.iast.SourceTypes.REQUEST_QUERY +import static datadog.trace.api.iast.SourceTypes.SQL_TABLE import static datadog.trace.api.iast.VulnerabilityMarks.NOT_MARKED import static com.datadog.iast.taint.Ranges.mergeRanges import static datadog.trace.api.iast.SourceTypes.REQUEST_HEADER_NAME @@ -378,6 +380,24 @@ class RangesTest extends DDSpecification { 1 | 3 | 2 | range(8, 8) | 0 | 0 | [] } + void 'test excludeRangesBySource method'() { + when: + final result = Ranges.excludeRangesBySource(ranges as Range[], source as BitSet) + + then: + final expectedArray = expected as Range[] + result == expectedArray + + where: + ranges | source | expected + [rangeWithSource(0, 5, SQL_TABLE), range(5, 3)] | bitSetOf(SQL_TABLE) | [range(5, 3)] + [rangeWithSource(0, 5, SQL_TABLE), range(5, 3)] | bitSetOf(SQL_TABLE, REQUEST_QUERY) | [range(5, 3)] + [rangeWithSource(0, 5, SQL_TABLE), range(5, 3)] | bitSetOf(REQUEST_HEADER_NAME) | [rangeWithSource(0, 5, SQL_TABLE)] + [rangeWithSource(0, 5, SQL_TABLE), range(5, 3)] | bitSetOf(REQUEST_QUERY) | [rangeWithSource(0, 5, SQL_TABLE), range(5, 3)] + [rangeWithSource(0, 5, SQL_TABLE), range(5, 3)] | bitSetOf(REQUEST_QUERY, REQUEST_HEADER_NAME) | [rangeWithSource(0, 5, SQL_TABLE)] + [] | bitSetOf(SQL_TABLE) | [] + } + Range[] rangesFromSpec(List> spec) { def ranges = new Range[spec.size()] int j = 0 @@ -417,4 +437,12 @@ class RangesTest extends DDSpecification { Range rangeWithSource(final int start, final int length, final byte source, final String name = 'name', final String value = 'value') { return new Range(start, length, new Source(source, name, value), NOT_MARKED) } + + BitSet bitSetOf(byte... values) { + BitSet bitSet = new BitSet() + for (byte value : values) { + bitSet.set(value) + } + return bitSet + } } diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/IastResultSetInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/IastResultSetInstrumentation.java new file mode 100644 index 00000000000..5a6447613ba --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/IastResultSetInstrumentation.java @@ -0,0 +1,117 @@ +package datadog.trace.instrumentation.jdbc; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface; +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.advice.ActiveRequestContext; +import datadog.trace.advice.RequiresRequestContext; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.Config; +import datadog.trace.api.gateway.RequestContext; +import datadog.trace.api.gateway.RequestContextSlot; +import datadog.trace.api.iast.IastContext; +import datadog.trace.api.iast.InstrumentationBridge; +import datadog.trace.api.iast.Source; +import datadog.trace.api.iast.SourceTypes; +import datadog.trace.api.iast.propagation.PropagationModule; +import datadog.trace.bootstrap.CallDepthThreadLocalMap; +import datadog.trace.bootstrap.ContextStore; +import datadog.trace.bootstrap.InstrumentationContext; +import java.sql.ResultSet; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public class IastResultSetInstrumentation extends InstrumenterModule.Iast + implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { + + public IastResultSetInstrumentation() { + super("jdbc", "jdbc-resultset", "iast-resultset"); + } + + @Override + public String hierarchyMarkerType() { + return "java.sql.ResultSet"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return implementsInterface(named("java.sql.ResultSet")); + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod().and(named("next").and(takesArguments(0))), + IastResultSetInstrumentation.class.getName() + "$NextAdvice"); + transformer.applyAdvice( + isMethod() + .and(named("getString").or(named("getNString"))) + .and(takesArguments(int.class).or(takesArguments(String.class))), + IastResultSetInstrumentation.class.getName() + "$GetParameterAdvice"); + } + + @Override + public Map contextStore() { + return singletonMap("java.sql.ResultSet", Integer.class.getName()); + } + + public static class NextAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This final ResultSet resultSet) { + ContextStore contextStore = + InstrumentationContext.get(ResultSet.class, Integer.class); + if (contextStore.get(resultSet) != null) { + contextStore.put(resultSet, contextStore.get(resultSet) + 1); + } else { + // first time + contextStore.put(resultSet, 1); + } + } + } + + @RequiresRequestContext(RequestContextSlot.IAST) + public static class GetParameterAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter() { + CallDepthThreadLocalMap.incrementCallDepth(ResultSet.class); + } + + @Advice.OnMethodExit(suppress = Throwable.class) + @Source(SourceTypes.SQL_TABLE) + public static void onExit( + @Advice.Argument(0) Object argument, + @Advice.Return final String value, + @Advice.This final ResultSet resultSet, + @ActiveRequestContext RequestContext reqCtx) { + if (CallDepthThreadLocalMap.decrementCallDepth(ResultSet.class) > 0) { + return; + } + ContextStore contextStore = + InstrumentationContext.get(ResultSet.class, Integer.class); + if (contextStore.get(resultSet) > Config.get().getIastDbRowsToTaint()) { + return; + } + if (value == null) { + return; + } + final PropagationModule module = InstrumentationBridge.PROPAGATION; + if (module == null) { + return; + } + IastContext ctx = reqCtx.getData(RequestContextSlot.IAST); + if (argument instanceof String) { + module.taintString(ctx, value, SourceTypes.SQL_TABLE, (String) argument); + } else { + module.taintString(ctx, value, SourceTypes.SQL_TABLE); + } + } + } +} diff --git a/dd-java-agent/instrumentation/jdbc/src/test/groovy/IastResultSetInstrumentationTest.groovy b/dd-java-agent/instrumentation/jdbc/src/test/groovy/IastResultSetInstrumentationTest.groovy new file mode 100644 index 00000000000..bcad0b535f8 --- /dev/null +++ b/dd-java-agent/instrumentation/jdbc/src/test/groovy/IastResultSetInstrumentationTest.groovy @@ -0,0 +1,254 @@ +import com.datadog.iast.taint.TaintedObject +import com.datadog.iast.test.IastAgentTestRunner +import datadog.trace.api.iast.SourceTypes +import datadog.trace.bootstrap.CallDepthThreadLocalMap +import foo.bar.IastInstrumentedConnection +import spock.lang.Shared + +import javax.sql.DataSource +import java.sql.Connection +import java.sql.ResultSet + +import static datadog.trace.api.config.IastConfig.IAST_DB_ROWS_TO_TAINT + +class IastResultSetInstrumentationTest extends IastAgentTestRunner { + @Shared + DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource( + url: 'jdbc:h2:mem:iastUnitTest', + driverClassName: 'org.h2.Driver', + password: '', + maxActive: 1, + ) + + private Connection connection + Connection getConnection() { + connection = (connection ?: dataSource.connection) + } + + void cleanup() { + connection?.close() + } + + void setupSpec() { + dataSource.connection.withCloseable { + it.createStatement().withCloseable {stmt -> + stmt.executeUpdate('CREATE TABLE TEST (id integer NOT NULL, name VARCHAR NOT NULL)') + stmt.executeUpdate('INSERT INTO TEST VALUES(42, \'foo\')') + stmt.executeUpdate('INSERT INTO TEST VALUES(43, \'bar\')') + } + } + } + + void cleanupSpec() { + dataSource.close(true) + } + + void 'returned string is tainted with source values as sql table'() { + when: + def valueRead + TaintedObject taintedObject + runUnderIastTrace { + String sql = 'SELECT name FROM TEST LIMIT 1' + Connection constWrapper = new IastInstrumentedConnection(conn: connection) + + constWrapper.prepareStatement(sql).withCloseable { stmt -> + stmt.executeQuery().withCloseable { rs -> + if (rs.next()) { + valueRead = rs.getString(1) + } + } + } + + taintedObject = localTaintedObjects.get(valueRead) + } + + then: + CallDepthThreadLocalMap.getCallDepth(ResultSet) == 0 + valueRead == "foo" + taintedObject != null + with(taintedObject) { + with(ranges[0]) { + source.origin == SourceTypes.SQL_TABLE + source.name == null + source.value == valueRead + } + } + } + + void 'returned string is tainted with source values as sql table but second value is not tainted'() { + when: + List valuesRead = [] + String column = "name" + List taintedObjects = [] + runUnderIastTrace { + String sql = 'SELECT name FROM TEST LIMIT 2' + Connection constWrapper = new IastInstrumentedConnection(conn: connection) + + constWrapper.prepareStatement(sql).withCloseable { stmt -> + stmt.executeQuery().withCloseable { rs -> + while (rs.next()) { + valuesRead.add(rs.getString(column)) + } + } + } + + taintedObjects.add(localTaintedObjects.get(valuesRead[0])) + taintedObjects.add(localTaintedObjects.get(valuesRead[1])) + } + + then: + CallDepthThreadLocalMap.getCallDepth(ResultSet) == 0 + valuesRead[0] == "foo" + taintedObjects[0] != null + with(taintedObjects[0]) { + with(ranges[0]) { + source.origin == SourceTypes.SQL_TABLE + source.name == column + source.value == valuesRead[0] + } + } + valuesRead[1] == "bar" + taintedObjects[1] == null + } + + void 'returned string is tainted with source values as sql table and second value in the same row is tainted'() { + when: + List valuesRead = [] + String column = "name" + List taintedObjects = [] + runUnderIastTrace { + String sql = 'SELECT name FROM TEST LIMIT 1' + Connection constWrapper = new IastInstrumentedConnection(conn: connection) + + constWrapper.prepareStatement(sql).withCloseable { stmt -> + stmt.executeQuery().withCloseable { rs -> + while (rs.next()) { + valuesRead.add(rs.getString(column)) + valuesRead.add(rs.getString(column)) + } + } + } + + taintedObjects.add(localTaintedObjects.get(valuesRead[0])) + taintedObjects.add(localTaintedObjects.get(valuesRead[1])) + } + + then: + CallDepthThreadLocalMap.getCallDepth(ResultSet) == 0 + valuesRead[0] == "foo" + taintedObjects[0] != null + with(taintedObjects[0]) { + with(ranges[0]) { + source.origin == SourceTypes.SQL_TABLE + source.name == column + source.value == valuesRead[0] + } + } + valuesRead[1] == "foo" + taintedObjects[1] != null + with(taintedObjects[1]) { + with(ranges[0]) { + source.origin == SourceTypes.SQL_TABLE + source.name == column + source.value == valuesRead[1] + } + } + } + + void 'returned string is tainted with source values as sql table and can taint up to two values'() { + given: + injectSysConfig(IAST_DB_ROWS_TO_TAINT, "2") + + when: + List valuesRead = [] + String column = "name" + List taintedObjects = [] + runUnderIastTrace { + String sql = 'SELECT name FROM TEST LIMIT 2' + Connection constWrapper = new IastInstrumentedConnection(conn: connection) + + constWrapper.prepareStatement(sql).withCloseable { stmt -> + stmt.executeQuery().withCloseable { rs -> + while (rs.next()) { + valuesRead.add(rs.getString(column)) + } + } + } + + taintedObjects.add(localTaintedObjects.get(valuesRead[0])) + taintedObjects.add(localTaintedObjects.get(valuesRead[1])) + } + + then: + CallDepthThreadLocalMap.getCallDepth(ResultSet) == 0 + valuesRead[0] == "foo" + taintedObjects[0] != null + with(taintedObjects[0]) { + with(ranges[0]) { + source.origin == SourceTypes.SQL_TABLE + source.name == column + source.value == valuesRead[0] + } + } + valuesRead[1] == "bar" + taintedObjects[1] != null + with(taintedObjects[1]) { + with(ranges[0]) { + source.origin == SourceTypes.SQL_TABLE + source.name == column + source.value == valuesRead[1] + } + } + } + + void 'when returned value is not a string does not taint the result' () { + when: + def valueRead + TaintedObject taintedObject + runUnderIastTrace { + String sql = 'SELECT id FROM TEST LIMIT 1' + Connection constWrapper = new IastInstrumentedConnection(conn: connection) + + constWrapper.prepareStatement(sql).withCloseable { stmt -> + stmt.executeQuery().withCloseable { rs -> + if (rs.next()) { + valueRead = rs.getInt(1) + } + } + } + + taintedObject = localTaintedObjects.get(valueRead) + } + + then: + CallDepthThreadLocalMap.getCallDepth(ResultSet) == 0 + valueRead == 42 + taintedObject == null + } + + void 'when CallDepthThreadLocalMap is greater than 0 does not taint the values' () { + when: + def valueRead + TaintedObject taintedObject + CallDepthThreadLocalMap.incrementCallDepth(ResultSet) + runUnderIastTrace { + String sql = 'SELECT name FROM TEST LIMIT 1' + Connection constWrapper = new IastInstrumentedConnection(conn: connection) + + constWrapper.prepareStatement(sql).withCloseable { stmt -> + stmt.executeQuery().withCloseable { rs -> + if (rs.next()) { + valueRead = rs.getString(1) + } + } + } + + taintedObject = localTaintedObjects.get(valueRead) + } + + then: + CallDepthThreadLocalMap.getCallDepth(ResultSet) == 1 + valueRead == "foo" + taintedObject == null + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index fe360b81dd8..816a9c0145b 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -136,6 +136,7 @@ public final class ConfigDefaults { static final boolean DEFAULT_IAST_ANONYMOUS_CLASSES_ENABLED = true; static final boolean DEFAULT_IAST_STACK_TRACE_ENABLED = true; + static final int DEFAULT_IAST_DB_ROWS_TO_TAINT = 1; static final boolean DEFAULT_LLM_OBS_ENABLED = false; static final boolean DEFAULT_LLM_OBS_AGENTLESS_ENABLED = false; 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 a986535f0bc..3207b1ccdf4 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 @@ -29,6 +29,7 @@ public final class IastConfig { public static final String IAST_STACK_TRACE_ENABLED = "iast.stacktrace.enabled"; public static final String IAST_SECURITY_CONTROLS_CONFIGURATION = "iast.security-controls.configuration"; + public static final String IAST_DB_ROWS_TO_TAINT = "iast.db.rows-to-taint"; private IastConfig() {} } 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 02b78b68031..5cf5e4edef8 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -309,6 +309,7 @@ public static String getHostName() { private final boolean iastStackTraceEnabled; private final boolean iastExperimentalPropagationEnabled; private final String iastSecurityControlsConfiguration; + private final int iastDbRowsToTaint; private final boolean llmObsAgentlessEnabled; private final String llmObsMlApp; @@ -1339,6 +1340,8 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) configProvider.getBoolean(IAST_EXPERIMENTAL_PROPAGATION_ENABLED, false); iastSecurityControlsConfiguration = configProvider.getString(IAST_SECURITY_CONTROLS_CONFIGURATION, null); + iastDbRowsToTaint = + configProvider.getInteger(IAST_DB_ROWS_TO_TAINT, DEFAULT_IAST_DB_ROWS_TO_TAINT); llmObsAgentlessEnabled = configProvider.getBoolean(LLMOBS_AGENTLESS_ENABLED, DEFAULT_LLM_OBS_AGENTLESS_ENABLED); @@ -2673,6 +2676,10 @@ public String getIastSecurityControlsConfiguration() { return iastSecurityControlsConfiguration; } + public int getIastDbRowsToTaint() { + return iastDbRowsToTaint; + } + public boolean isLlmObsEnabled() { return instrumenterConfig.isLlmObsEnabled(); } diff --git a/internal-api/src/main/java/datadog/trace/api/iast/SourceTypes.java b/internal-api/src/main/java/datadog/trace/api/iast/SourceTypes.java index 1c3dc208d2a..515cab6f252 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/SourceTypes.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/SourceTypes.java @@ -24,6 +24,7 @@ private SourceTypes() {} public static final byte GRPC_BODY = 13; public static final byte KAFKA_MESSAGE_KEY = 14; public static final byte KAFKA_MESSAGE_VALUE = 15; + public static final byte SQL_TABLE = 16; /** Array indexed with all source types, the index should match the source types values */ public static final String[] STRINGS = { @@ -42,7 +43,8 @@ private SourceTypes() {} "http.request.path", "grpc.request.body", "kafka.message.key", - "kafka.message.value" + "kafka.message.value", + "sql.row.value" }; public static String toString(final byte source) {