diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java index 589b2dfc80e..5da60ff4d30 100644 --- a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/IastSystem.java @@ -14,6 +14,7 @@ import com.datadog.iast.sink.PathTraversalModuleImpl; import com.datadog.iast.sink.SqlInjectionModuleImpl; import com.datadog.iast.sink.SsrfModuleImpl; +import com.datadog.iast.sink.StacktraceLeakModuleImpl; import com.datadog.iast.sink.TrustBoundaryViolationModuleImpl; import com.datadog.iast.sink.UnvalidatedRedirectModuleImpl; import com.datadog.iast.sink.WeakCipherModuleImpl; @@ -100,7 +101,8 @@ private static Stream iastModules(final Dependencies dependencies) { new WeakRandomnessModuleImpl(dependencies), new XPathInjectionModuleImpl(dependencies), new TrustBoundaryViolationModuleImpl(dependencies), - new XssModuleImpl(dependencies)); + new XssModuleImpl(dependencies), + new StacktraceLeakModuleImpl(dependencies)); } private static void registerRequestStartedCallback( 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 ae1d25ddfb3..62fdc6db56e 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 @@ -65,6 +65,9 @@ public interface VulnerabilityType { InjectionType XSS = new InjectionTypeImpl(VulnerabilityTypes.XSS_STRING, VulnerabilityMarks.XSS_MARK, ' '); + VulnerabilityType STACKTRACE_LEAK = + new VulnerabilityTypeImpl(VulnerabilityTypes.STACKTRACE_LEAK_STRING, NOT_MARKED); + String name(); /** A bit flag to ignore tainted ranges for this vulnerability. Set to 0 if none. */ diff --git a/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/StacktraceLeakModuleImpl.java b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/StacktraceLeakModuleImpl.java new file mode 100644 index 00000000000..2cfb07e5f49 --- /dev/null +++ b/dd-java-agent/agent-iast/src/main/java/com/datadog/iast/sink/StacktraceLeakModuleImpl.java @@ -0,0 +1,37 @@ +package com.datadog.iast.sink; + +import com.datadog.iast.Dependencies; +import com.datadog.iast.model.Evidence; +import com.datadog.iast.model.Location; +import com.datadog.iast.model.Vulnerability; +import com.datadog.iast.model.VulnerabilityType; +import datadog.trace.api.iast.sink.StacktraceLeakModule; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import org.jetbrains.annotations.NotNull; + +public class StacktraceLeakModuleImpl extends SinkModuleBase implements StacktraceLeakModule { + + public StacktraceLeakModuleImpl(@NotNull Dependencies dependencies) { + super(dependencies); + } + + @Override + public void onStacktraceLeak( + Throwable throwable, String moduleName, String className, String methodName) { + if (throwable != null) { + final AgentSpan span = AgentTracer.activeSpan(); + + Evidence evidence = + new Evidence( + "ExceptionHandler in " + + moduleName + + " \r\nthrown " + + throwable.getClass().getName()); + Location location = Location.forSpanAndClassAndMethod(span, className, methodName); + + reporter.report( + span, new Vulnerability(VulnerabilityType.STACKTRACE_LEAK, location, evidence)); + } + } +} diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/StacktraceLeakModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/StacktraceLeakModuleTest.groovy new file mode 100644 index 00000000000..ecf8aaa3bd0 --- /dev/null +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/StacktraceLeakModuleTest.groovy @@ -0,0 +1,51 @@ +package com.datadog.iast.sink + +import com.datadog.iast.IastModuleImplTestBase +import com.datadog.iast.model.Evidence +import com.datadog.iast.model.Vulnerability +import com.datadog.iast.model.VulnerabilityType +import datadog.trace.api.iast.sink.StacktraceLeakModule +import datadog.trace.bootstrap.instrumentation.api.AgentSpan + +class StacktraceLeakModuleTest extends IastModuleImplTestBase { + private StacktraceLeakModule module + + def setup() { + module = new StacktraceLeakModuleImpl(dependencies) + } + + void 'iast stacktrace leak module'() { + given: + final spanId = 123456 + final span = Mock(AgentSpan) + + def throwable = new Exception('some exception') + def moduleName = 'moduleName' + def className = 'className' + def methodName = 'methodName' + + when: + module.onStacktraceLeak(throwable, moduleName, className, methodName) + + then: + 1 * tracer.activeSpan() >> span + 1 * span.getSpanId() >> spanId + 1 * span.getServiceName() + 1 * reporter.report(_, _) >> { args -> + Vulnerability vuln = args[1] as Vulnerability + assert vuln != null + assert vuln.getType() == VulnerabilityType.STACKTRACE_LEAK + assert vuln.getEvidence() == new Evidence('ExceptionHandler in moduleName \r\nthrown java.lang.Exception') + assert vuln.getLocation() != null + } + 0 * _ + } + + void 'iast stacktrace leak no exception'() { + when: + module.onStacktraceLeak(null, null, null, null) + + then: + 0 * _ + } +} diff --git a/dd-java-agent/instrumentation/tomcat-appsec-7/src/main/java/datadog/trace/instrumentation/tomcat7/ErrorReportValueAdvice.java b/dd-java-agent/instrumentation/tomcat-appsec-7/src/main/java/datadog/trace/instrumentation/tomcat7/ErrorReportValueAdvice.java new file mode 100644 index 00000000000..6d5cf633435 --- /dev/null +++ b/dd-java-agent/instrumentation/tomcat-appsec-7/src/main/java/datadog/trace/instrumentation/tomcat7/ErrorReportValueAdvice.java @@ -0,0 +1,78 @@ +package datadog.trace.instrumentation.tomcat7; + +import static datadog.trace.bootstrap.blocking.BlockingActionHelper.TemplateType.HTML; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; + +import datadog.trace.api.Config; +import datadog.trace.api.iast.InstrumentationBridge; +import datadog.trace.api.iast.sink.StacktraceLeakModule; +import datadog.trace.bootstrap.blocking.BlockingActionHelper; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import net.bytebuddy.asm.Advice; +import org.apache.catalina.connector.Response; + +public class ErrorReportValueAdvice { + + @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) + public static boolean onEnter( + @Advice.Argument(value = 1) Response response, + @Advice.Argument(value = 2) Throwable throwable, + @Advice.Origin("#t") String className, + @Advice.Origin("#m") String methodName) { + int statusCode = response.getStatus(); + + // Do nothing on a 1xx, 2xx, 3xx and 404 status + // Do nothing if the response hasn't been explicitly marked as in error + // and that error has not been reported. + if (statusCode < 400 || statusCode == 404 || !response.isError()) { + return true; // skip original method + } + + final AgentSpan span = activeSpan(); + if (span != null && throwable != null) { + // Report IAST + final StacktraceLeakModule module = InstrumentationBridge.STACKTRACE_LEAK_MODULE; + if (module != null) { + try { + module.onStacktraceLeak(throwable, "Tomcat 7+", className, methodName); + } catch (final Throwable e) { + module.onUnexpectedException("onResponseException threw", e); + } + } + } + + // If we don't need to suppress stacktrace leak + if (!Config.get().isIastStacktraceLeakSuppress()) { + return false; + } + + byte[] template = BlockingActionHelper.getTemplate(HTML); + if (template == null) { + return false; + } + + try { + try { + String contentType = BlockingActionHelper.getContentType(HTML); + response.setContentType(contentType); + } catch (Throwable t) { + // Ignore + } + Writer writer = response.getReporter(); + if (writer != null) { + // If writer is null, it's an indication that the response has + // been hard committed already, which should never happen + String html = new String(template, StandardCharsets.UTF_8); + writer.write(html); + response.finishResponse(); + } + } catch (IOException | IllegalStateException e) { + // Ignore + } + + return false; + } +} diff --git a/dd-java-agent/instrumentation/tomcat-appsec-7/src/main/java/datadog/trace/instrumentation/tomcat7/ErrorReportValueInstrumentation.java b/dd-java-agent/instrumentation/tomcat-appsec-7/src/main/java/datadog/trace/instrumentation/tomcat7/ErrorReportValueInstrumentation.java new file mode 100644 index 00000000000..117ce32f082 --- /dev/null +++ b/dd-java-agent/instrumentation/tomcat-appsec-7/src/main/java/datadog/trace/instrumentation/tomcat7/ErrorReportValueInstrumentation.java @@ -0,0 +1,40 @@ +package datadog.trace.instrumentation.tomcat7; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; + +@AutoService(Instrumenter.class) +public class ErrorReportValueInstrumentation extends Instrumenter.Iast + implements Instrumenter.ForSingleType { + + public ErrorReportValueInstrumentation() { + super("tomcat"); + } + + @Override + public String muzzleDirective() { + return "from7"; + } + + @Override + public String instrumentedType() { + return "org.apache.catalina.valves.ErrorReportValve"; + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + isMethod() + .and(named("report")) + .and(takesArgument(0, named("org.apache.catalina.connector.Request"))) + .and(takesArgument(1, named("org.apache.catalina.connector.Response"))) + .and(takesArgument(2, Throwable.class)) + .and(isProtected()), + packageName + ".ErrorReportValueAdvice"); + } +} diff --git a/dd-smoke-tests/appsec/spring-tomcat7/build.gradle b/dd-smoke-tests/appsec/spring-tomcat7/build.gradle new file mode 100644 index 00000000000..bb3b9ddf5e9 --- /dev/null +++ b/dd-smoke-tests/appsec/spring-tomcat7/build.gradle @@ -0,0 +1,33 @@ +plugins { + id "com.github.johnrengelman.shadow" +} + +apply from: "$rootDir/gradle/java.gradle" +description = 'Spring Tomcat7 Smoke Tests.' + +jar { + manifest { + attributes('Main-Class': 'datadog.smoketest.appsec.springtomcat7.Main') + } +} + +dependencies { + implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '7.0.47' + implementation group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '7.0.47' + implementation group: 'org.apache.tomcat', name: 'tomcat-juli', version: '7.0.47' + implementation group: 'org.springframework', name: 'spring-webmvc', version: '4.0.0.RELEASE' + + testImplementation project(':dd-smoke-tests:appsec') +} + +tasks.withType(Test).configureEach { + dependsOn "shadowJar" + + jvmArgs "-Ddatadog.smoketest.appsec.springtomcat7.shadowJar.path=${tasks.shadowJar.archiveFile.get()}" +} + +task testRuntimeActivation(type: Test) { + jvmArgs '-Dsmoke_test.appsec.enabled=inactive', + "-Ddatadog.smoketest.appsec.springtomcat7.shadowJar.path=${tasks.shadowJar.archiveFile.get()}" +} +tasks['check'].dependsOn(testRuntimeActivation) diff --git a/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/AppConfigurer.java b/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/AppConfigurer.java new file mode 100644 index 00000000000..f32a9d67b82 --- /dev/null +++ b/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/AppConfigurer.java @@ -0,0 +1,11 @@ +package datadog.smoketest.appsec.springtomcat7; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +@Configuration +@EnableWebMvc +@ComponentScan(basePackages = {"datadog.smoketest.appsec.springtomcat7"}) +public class AppConfigurer extends WebMvcConfigurerAdapter {} diff --git a/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/Controller.java b/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/Controller.java new file mode 100644 index 00000000000..54e64878cf6 --- /dev/null +++ b/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/Controller.java @@ -0,0 +1,18 @@ +package datadog.smoketest.appsec.springtomcat7; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class Controller { + + @RequestMapping("/") + public String htmlString() { + return "Hello world!"; + } + + @RequestMapping("/exception") + public void exceptionMethod() throws Throwable { + throw new Throwable("hello"); + } +} diff --git a/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/Main.java b/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/Main.java new file mode 100644 index 00000000000..629bd4e8ef5 --- /dev/null +++ b/dd-smoke-tests/appsec/spring-tomcat7/src/main/java/datadog/smoketest/appsec/springtomcat7/Main.java @@ -0,0 +1,56 @@ +package datadog.smoketest.appsec.springtomcat7; + +import de.thetaphi.forbiddenapis.SuppressForbidden; +import java.io.File; +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +public class Main { + + private static final String ROOT = "/"; + private static final String SERVLET = "dispatcherServlet"; + + @SuppressForbidden + public static void main(String[] args) throws Exception { + int port = 8080; + for (String arg : args) { + if (arg.contains("=")) { + String[] kv = arg.split("="); + if (kv.length == 2) { + if ("--server.port".equalsIgnoreCase(kv[0])) { + try { + port = Integer.parseInt(kv[1]); + } catch (NumberFormatException e) { + System.out.println( + "--server.port '" + + kv[1] + + "' is not valid port. Will be used default port " + + port); + } + } + } + } + } + + Tomcat tomcat = new Tomcat(); + tomcat.setPort(port); + + Context context = tomcat.addContext(ROOT, new File(".").getAbsolutePath()); + + Tomcat.addServlet( + context, + SERVLET, + new DispatcherServlet( + new AnnotationConfigWebApplicationContext() { + { + register(AppConfigurer.class); + } + })); + context.addServletMapping(ROOT, SERVLET); + + tomcat.start(); + tomcat.getServer().await(); + } +} diff --git a/dd-smoke-tests/appsec/spring-tomcat7/src/test/groovy/datadog/smoketest/appsec/SpringTomcatSmokeTest.groovy b/dd-smoke-tests/appsec/spring-tomcat7/src/test/groovy/datadog/smoketest/appsec/SpringTomcatSmokeTest.groovy new file mode 100644 index 00000000000..75e7168cd3f --- /dev/null +++ b/dd-smoke-tests/appsec/spring-tomcat7/src/test/groovy/datadog/smoketest/appsec/SpringTomcatSmokeTest.groovy @@ -0,0 +1,36 @@ +package datadog.smoketest.appsec + +import okhttp3.Request + +class SpringTomcatSmokeTest extends AbstractAppSecServerSmokeTest { + + @Override + ProcessBuilder createProcessBuilder() { + String springBootShadowJar = System.getProperty("datadog.smoketest.appsec.springtomcat7.shadowJar.path") + + List command = new ArrayList<>() + command.add(javaPath()) + command.addAll(defaultJavaProperties) + command.add("-Ddd.iast.enabled=true") + command.add("-Ddd.iast.stacktrace-leak.suppress=true") + command.addAll((String[]) ["-jar", springBootShadowJar, "--server.port=${httpPort}"]) + + ProcessBuilder processBuilder = new ProcessBuilder(command) + processBuilder.directory(new File(buildDirectory)) + } + + def "suppress exception stacktrace"() { + when: + String url = "http://localhost:${httpPort}/exception" + def request = new Request.Builder() + .url(url) + .build() + def response = client.newCall(request).execute() + def responseBodyStr = response.body().string() + waitForTraceCount 1 + + then: + responseBodyStr.contains('Sorry, you cannot access this page. Please contact the customer service team.') + response.code() == 500 + } +} \ No newline at end of file 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 24cd05cacdf..940d44fb5da 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 @@ -105,6 +105,7 @@ public final class ConfigDefaults { static final String DEFAULT_IAST_REDACTION_VALUE_PATTERN = "(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,}))"; public static final int DEFAULT_IAST_MAX_RANGE_COUNT = 10; + static final boolean DEFAULT_IAST_STACKTRACE_LEAK_SUPPRESS = false; static final int DEFAULT_IAST_TRUNCATION_MAX_VALUE_LENGTH = 250; public static final boolean DEFAULT_IAST_DEDUPLICATION_ENABLED = true; 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 3b6aa5c284c..286533c8230 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 @@ -16,6 +16,7 @@ public final class IastConfig { public static final String IAST_REDACTION_ENABLED = "iast.redaction.enabled"; public static final String IAST_REDACTION_NAME_PATTERN = "iast.redaction.name.pattern"; public static final String IAST_REDACTION_VALUE_PATTERN = "iast.redaction.value.pattern"; + public static final String IAST_STACKTRACE_LEAK_SUPPRESS = "iast.stacktrace-leak.suppress"; public static final String IAST_MAX_RANGE_COUNT = "iast.max-range-count"; public static final String IAST_TRUNCATION_MAX_VALUE_LENGTH = "iast.trunctation.max.value.length"; 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 5f0aceaef72..3f252209497 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -68,6 +68,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_REDACTION_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_REDACTION_NAME_PATTERN; import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_REDACTION_VALUE_PATTERN; +import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_STACKTRACE_LEAK_SUPPRESS; import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_TRUNCATION_MAX_VALUE_LENGTH; import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_WEAK_CIPHER_ALGORITHMS; import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_WEAK_HASH_ALGORITHMS; @@ -237,6 +238,7 @@ import static datadog.trace.api.config.IastConfig.IAST_REDACTION_ENABLED; import static datadog.trace.api.config.IastConfig.IAST_REDACTION_NAME_PATTERN; import static datadog.trace.api.config.IastConfig.IAST_REDACTION_VALUE_PATTERN; +import static datadog.trace.api.config.IastConfig.IAST_STACKTRACE_LEAK_SUPPRESS; import static datadog.trace.api.config.IastConfig.IAST_TELEMETRY_VERBOSITY; import static datadog.trace.api.config.IastConfig.IAST_TRUNCATION_MAX_VALUE_LENGTH; import static datadog.trace.api.config.IastConfig.IAST_WEAK_CIPHER_ALGORITHMS; @@ -677,6 +679,7 @@ static class HostNameHolder { private final String iastRedactionValuePattern; private final int iastMaxRangeCount; private final int iastTruncationMaxValueLength; + private final boolean iastStacktraceLeakSuppress; private final boolean ciVisibilityTraceSanitationEnabled; private final boolean ciVisibilityAgentlessEnabled; @@ -1532,6 +1535,9 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins configProvider.getInteger( IAST_TRUNCATION_MAX_VALUE_LENGTH, DEFAULT_IAST_TRUNCATION_MAX_VALUE_LENGTH); iastMaxRangeCount = iastDetectionMode.getIastMaxRangeCount(configProvider); + iastStacktraceLeakSuppress = + configProvider.getBoolean( + IAST_STACKTRACE_LEAK_SUPPRESS, DEFAULT_IAST_STACKTRACE_LEAK_SUPPRESS); ciVisibilityTraceSanitationEnabled = configProvider.getBoolean(CIVISIBILITY_TRACE_SANITATION_ENABLED, true); @@ -2563,6 +2569,10 @@ public int getIastMaxRangeCount() { return iastMaxRangeCount; } + public boolean isIastStacktraceLeakSuppress() { + return iastStacktraceLeakSuppress; + } + public boolean isCiVisibilityEnabled() { return instrumenterConfig.isCiVisibilityEnabled(); } diff --git a/internal-api/src/main/java/datadog/trace/api/iast/InstrumentationBridge.java b/internal-api/src/main/java/datadog/trace/api/iast/InstrumentationBridge.java index d00e5473f2e..459ba10196a 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/InstrumentationBridge.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/InstrumentationBridge.java @@ -13,6 +13,7 @@ import datadog.trace.api.iast.sink.PathTraversalModule; import datadog.trace.api.iast.sink.SqlInjectionModule; import datadog.trace.api.iast.sink.SsrfModule; +import datadog.trace.api.iast.sink.StacktraceLeakModule; import datadog.trace.api.iast.sink.TrustBoundaryViolationModule; import datadog.trace.api.iast.sink.UnvalidatedRedirectModule; import datadog.trace.api.iast.sink.WeakCipherModule; @@ -51,6 +52,8 @@ public abstract class InstrumentationBridge { public static volatile XssModule XSS; + public static volatile StacktraceLeakModule STACKTRACE_LEAK_MODULE; + private InstrumentationBridge() {} public static void registerIastModule(final IastModule module) { @@ -96,6 +99,8 @@ public static void registerIastModule(final IastModule module) { TRUST_BOUNDARY_VIOLATION = (TrustBoundaryViolationModule) module; } else if (module instanceof XssModule) { XSS = (XssModule) module; + } else if (module instanceof StacktraceLeakModule) { + STACKTRACE_LEAK_MODULE = (StacktraceLeakModule) module; } else { throw new UnsupportedOperationException("Module not yet supported: " + module); } @@ -167,6 +172,9 @@ public static E getIastModule(final Class type) { if (type == XssModule.class) { return (E) XSS; } + if (type == StacktraceLeakModule.class) { + return (E) STACKTRACE_LEAK_MODULE; + } throw new UnsupportedOperationException("Module not yet supported: " + type); } @@ -193,5 +201,6 @@ public static void clearIastModules() { XPATH_INJECTION = null; TRUST_BOUNDARY_VIOLATION = null; XSS = null; + STACKTRACE_LEAK_MODULE = null; } } diff --git a/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityTypes.java b/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityTypes.java index 74e5a8ce22b..1a21ce795e5 100644 --- a/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityTypes.java +++ b/internal-api/src/main/java/datadog/trace/api/iast/VulnerabilityTypes.java @@ -38,6 +38,8 @@ private VulnerabilityTypes() {} public static final String TRUST_BOUNDARY_VIOLATION_STRING = "TRUST_BOUNDARY_VIOLATION"; public static final byte XSS = 16; public static final String XSS_STRING = "XSS"; + public static final byte STACKTRACE_LEAK = 17; + public static final String STACKTRACE_LEAK_STRING = "STACKTRACE_LEAK"; /** * Use for telemetry only, this is a special vulnerability type that is not reported, reported @@ -78,7 +80,8 @@ private VulnerabilityTypes() {} HSTS_HEADER_MISSING, XCONTENTTYPE_HEADER_MISSING, NO_SAMESITE_COOKIE, - XSS + XSS, + STACKTRACE_LEAK }; public static byte[] values() { @@ -121,6 +124,8 @@ public static String toString(final byte sourceType) { return VulnerabilityTypes.NO_SAMESITE_COOKIE_STRING; case VulnerabilityTypes.XSS: return VulnerabilityTypes.XSS_STRING; + case VulnerabilityTypes.STACKTRACE_LEAK: + return VulnerabilityTypes.STACKTRACE_LEAK_STRING; default: return null; } diff --git a/internal-api/src/main/java/datadog/trace/api/iast/sink/StacktraceLeakModule.java b/internal-api/src/main/java/datadog/trace/api/iast/sink/StacktraceLeakModule.java new file mode 100644 index 00000000000..2a6a668303c --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/iast/sink/StacktraceLeakModule.java @@ -0,0 +1,9 @@ +package datadog.trace.api.iast.sink; + +import datadog.trace.api.iast.IastModule; +import javax.annotation.Nullable; + +public interface StacktraceLeakModule extends IastModule { + void onStacktraceLeak( + @Nullable final Throwable expression, String moduleName, String className, String methodName); +} diff --git a/settings.gradle b/settings.gradle index d5917b55a68..80f4db89e19 100644 --- a/settings.gradle +++ b/settings.gradle @@ -141,6 +141,7 @@ include ':dd-smoke-tests:vertx-3.9-resteasy' include ':dd-smoke-tests:vertx-4.2' include ':dd-smoke-tests:wildfly' include ':dd-smoke-tests:appsec' +include ':dd-smoke-tests:appsec:spring-tomcat7' include ':dd-smoke-tests:appsec:springboot' include ':dd-smoke-tests:appsec:springboot-grpc' include ':dd-smoke-tests:appsec:springboot-security'