Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import static java.util.Collections.emptyMap;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
Expand All @@ -40,6 +41,7 @@
import datadog.trace.api.UserIdCollectionMode;
import datadog.trace.api.appsec.AppSecEventTracker;
import datadog.trace.api.function.TriFunction;
import datadog.trace.api.gateway.BlockResponseFunction;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.Flow;
import datadog.trace.api.gateway.RequestContext;
Expand All @@ -62,6 +64,8 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.tabletest.junit.TableTest;

@SuppressWarnings("deprecation") // exercises the deprecated v1 EventTracker API on purpose
Expand Down Expand Up @@ -457,6 +461,27 @@ void blockingOnALogin() {
() -> tracker.onLoginSuccessEvent(SDK, USER_LOGIN, USER_ID, METADATA));
}

@ParameterizedTest(name = "commit succeeds: {0}")
@ValueSource(booleans = {true, false})
void blockingOnALoginCommitsBlockingResponse(boolean commitResult) {
Flow.Action.RequestBlockingAction action =
new Flow.Action.RequestBlockingAction(403, BlockingContentType.JSON);
when(loginEvent.apply(isA(RequestContext.class), eq(LOGIN_SUCCESS), eq(USER_LOGIN)))
.thenReturn(new ActionFlow<>(action));
RecordingBlockResponseFunction brf = new RecordingBlockResponseFunction(commitResult);
when(requestContext.getBlockResponseFunction()).thenReturn(brf);
when(requestContext.getTraceSegment()).thenReturn(traceSegment);

assertThrows(
BlockingException.class,
() -> tracker.onLoginSuccessEvent(SDK, USER_LOGIN, USER_ID, METADATA));

assertEquals(1, brf.calls);
assertSame(traceSegment, brf.lastSegment);
assertEquals(action.getStatusCode(), brf.lastStatusCode);
assertEquals(action.getBlockingContentType(), brf.lastTemplateType);
}

@Test
void shouldNotFailOnNullCallback() {
when(provider.getCallback(EVENTS.user())).thenReturn(null);
Expand Down Expand Up @@ -575,6 +600,36 @@ public boolean isEnabled(UserIdCollectionMode mode) {
}
}

/**
* Hand-written fake that only implements the abstract 5-arg method, so it records the commit
* whichever {@code tryCommitBlockingResponse} overload the production code calls.
*/
private static final class RecordingBlockResponseFunction implements BlockResponseFunction {
private final boolean commitResult;
private int calls;
private TraceSegment lastSegment;
private int lastStatusCode;
private BlockingContentType lastTemplateType;

private RecordingBlockResponseFunction(boolean commitResult) {
this.commitResult = commitResult;
}

@Override
public boolean tryCommitBlockingResponse(
TraceSegment segment,
int statusCode,
BlockingContentType templateType,
Map<String, String> extraHeaders,
String securityResponseId) {
calls++;
lastSegment = segment;
lastStatusCode = statusCode;
lastTemplateType = templateType;
return commitResult;
}
}

private static class ActionFlow<T> implements Flow<T> {

private final Action action;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ tasks.named("compileJava11TestGroovy", GroovyCompile) {
dependencies {
testRuntimeOnly project(':dd-java-agent:instrumentation:datadog:asm:iast-instrumenter')
testImplementation group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '9.0.56'
testImplementation libs.bundles.mockito
java11TestImplementation sourceSets.test.output
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ org.junit.platform:junit-platform-runner:1.14.1=java11TestRuntimeClasspath,lates
org.junit.platform:junit-platform-suite-api:1.14.1=java11TestRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-suite-commons:1.14.1=java11TestRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath
org.junit:junit-bom:5.14.1=java11TestCompileClasspath,java11TestRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:4.4.0=java11TestRuntimeClasspath,latestDepTestRuntimeClasspath,testRuntimeClasspath
org.mockito:mockito-core:4.4.0=java11TestCompileClasspath,java11TestRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:4.4.0=java11TestCompileClasspath,java11TestRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=java11TestCompileClasspath,java11TestRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=java11TestCompileClasspath,java11TestRuntimeClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.10.1=spotbugs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package datadog.trace.instrumentation.java.lang;

import static datadog.trace.api.gateway.Events.EVENTS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import datadog.appsec.api.blocking.BlockingContentType;
import datadog.appsec.api.blocking.BlockingException;
import datadog.trace.api.gateway.BlockResponseFunction;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.Flow;
import datadog.trace.api.gateway.RequestContext;
import datadog.trace.api.gateway.RequestContextSlot;
import datadog.trace.api.internal.TraceSegment;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import java.io.File;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

/**
* Pins the fail-closed blocking behavior of the LFI RASP check: when the AppSec callback returns a
* real {@link Flow.Action.RequestBlockingAction}, a {@link BlockingException} is always thrown from
* every public entry point, whether or not a {@link BlockResponseFunction} is present and
* regardless of whether it managed to commit the blocking response.
*/
class FileIORaspHelperBlockingTest {

private static final Flow.Action.RequestBlockingAction RBA =
new Flow.Action.RequestBlockingAction(403, BlockingContentType.JSON);

private AgentTracer.TracerAPI originalTracer;
private FileIORaspHelper originalHelperInstance;
private TraceSegment traceSegment;
private RequestContext reqCtx;

/**
* Several pre-existing Groovy specs in this module replace {@link FileIORaspHelper#INSTANCE} with
* a mock and never restore it, leaking a permanently-neutered singleton into every test that runs
* afterwards in the same test JVM. Force a genuine instance for the duration of this test
* regardless of execution order, and restore whatever was there before.
*/
private static FileIORaspHelper newRealInstance() throws ReflectiveOperationException {
Constructor<FileIORaspHelper> ctor = FileIORaspHelper.class.getDeclaredConstructor();
ctor.setAccessible(true);
return ctor.newInstance();
}

@BeforeEach
void setUp() throws ReflectiveOperationException {
originalHelperInstance = FileIORaspHelper.INSTANCE;
FileIORaspHelper.INSTANCE = newRealInstance();

originalTracer = AgentTracer.get();
traceSegment = mock(TraceSegment.class);
reqCtx = mock(RequestContext.class);
when(reqCtx.getTraceSegment()).thenReturn(traceSegment);

AgentSpan span = mock(AgentSpan.class);
when(span.getRequestContext()).thenReturn(reqCtx);

@SuppressWarnings("unchecked")
Flow<Void> blockingFlow = mock(Flow.class);
when(blockingFlow.getAction()).thenReturn(RBA);
CallbackProvider callbackProvider = mock(CallbackProvider.class);
when(callbackProvider.getCallback(EVENTS.fileLoaded())).thenReturn((ctx, path) -> blockingFlow);
when(callbackProvider.getCallback(EVENTS.fileWritten()))
.thenReturn((ctx, path) -> blockingFlow);

AgentTracer.TracerAPI tracer = mock(AgentTracer.TracerAPI.class);
when(tracer.activeSpan()).thenReturn(span);
when(tracer.getCallbackProvider(RequestContextSlot.APPSEC)).thenReturn(callbackProvider);
AgentTracer.forceRegister(tracer);
}

@AfterEach
void tearDown() {
AgentTracer.forceRegister(originalTracer);
FileIORaspHelper.INSTANCE = originalHelperInstance;
}

static Stream<Arguments> entryPoints() {
// Read INSTANCE lazily inside each Executable, not here: @MethodSource is resolved by JUnit
// before @BeforeEach runs, so capturing INSTANCE at this point could still observe a mock
// leaked by an earlier Groovy spec in this module (see setUp()'s javadoc).
return Stream.of(
arguments(
"beforeFileLoaded(String)",
(Executable) () -> FileIORaspHelper.INSTANCE.beforeFileLoaded("f")),
arguments(
"beforeFileLoaded(String, String)",
(Executable) () -> FileIORaspHelper.INSTANCE.beforeFileLoaded("/tmp", "f")),
arguments(
"beforeFileLoaded(String, String[])",
(Executable)
() ->
FileIORaspHelper.INSTANCE.beforeFileLoaded("/tmp", new String[] {"log", "f"})),
arguments(
"beforeFileLoaded(File, String)",
(Executable) () -> FileIORaspHelper.INSTANCE.beforeFileLoaded(new File("/tmp"), "f")),
arguments(
"beforeFileLoaded(URI)",
(Executable)
() -> FileIORaspHelper.INSTANCE.beforeFileLoaded(URI.create("file:/tmp/f"))),
arguments(
"beforeFileWritten(String)",
(Executable) () -> FileIORaspHelper.INSTANCE.beforeFileWritten("f")),
arguments(
"beforeRandomAccessFileOpened(String, String)",
(Executable) () -> FileIORaspHelper.INSTANCE.beforeRandomAccessFileOpened("f", "rw")));
}

@ParameterizedTest(name = "{0}")
@MethodSource("entryPoints")
void throwsWithoutBlockResponseFunction(String name, Executable entryPoint) {
when(reqCtx.getBlockResponseFunction()).thenReturn(null);

assertThrows(BlockingException.class, entryPoint);
}

@ParameterizedTest(name = "{0}")
@MethodSource("entryPoints")
void throwsWhenBlockResponseFunctionCommits(String name, Executable entryPoint) {
assertThrowsAndCommits(entryPoint, true);
}

@ParameterizedTest(name = "{0}")
@MethodSource("entryPoints")
void throwsWhenBlockResponseFunctionFailsToCommit(String name, Executable entryPoint) {
assertThrowsAndCommits(entryPoint, false);
}

private void assertThrowsAndCommits(Executable entryPoint, boolean commitResult) {
RecordingBlockResponseFunction brf = new RecordingBlockResponseFunction(commitResult);
when(reqCtx.getBlockResponseFunction()).thenReturn(brf);

assertThrows(BlockingException.class, entryPoint);

brf.assertCommittedOnce(traceSegment);
}

/**
* Hand-written fake that only implements the abstract 5-arg method, so it records the commit
* whichever {@code tryCommitBlockingResponse} overload the production code calls.
*/
private static final class RecordingBlockResponseFunction implements BlockResponseFunction {
private final boolean commitResult;
private int calls;
private TraceSegment lastSegment;
private int lastStatusCode;
private BlockingContentType lastTemplateType;

private RecordingBlockResponseFunction(boolean commitResult) {
this.commitResult = commitResult;
}

@Override
public boolean tryCommitBlockingResponse(
TraceSegment segment,
int statusCode,
BlockingContentType templateType,
Map<String, String> extraHeaders,
String securityResponseId) {
calls++;
lastSegment = segment;
lastStatusCode = statusCode;
lastTemplateType = templateType;
return commitResult;
}

private void assertCommittedOnce(TraceSegment expectedSegment) {
// the first blocking event aborts the call, so a commit is attempted exactly once
assertEquals(1, calls);
assertSame(expectedSegment, lastSegment);
assertEquals(RBA.getStatusCode(), lastStatusCode);
assertEquals(RBA.getBlockingContentType(), lastTemplateType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ addTestSuiteForDir('latestDepTest', 'test')
dependencies {
testRuntimeOnly project(':dd-java-agent:instrumentation:datadog:asm:iast-instrumenter')
testImplementation group: 'org.springframework', name: 'spring-web', version: '4.3.7.RELEASE'
testImplementation libs.bundles.mockito
}

// IBM8 is having troubles with TLS set up by jetty 9.4.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ org.junit.platform:junit-platform-runner:1.14.1=latestDepTestRuntimeClasspath,te
org.junit.platform:junit-platform-suite-api:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-suite-commons:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath
org.junit:junit-bom:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-core:4.4.0=latestDepTestRuntimeClasspath,testRuntimeClasspath
org.mockito:mockito-core:4.4.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:4.4.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-analysis:9.10.1=spotbugs
Expand Down
Loading
Loading