diff --git a/sentry-core/src/main/java/io/sentry/core/PiiEventProcessor.java b/sentry-core/src/main/java/io/sentry/core/PiiEventProcessor.java new file mode 100644 index 000000000..678ce54d7 --- /dev/null +++ b/sentry-core/src/main/java/io/sentry/core/PiiEventProcessor.java @@ -0,0 +1,53 @@ +package io.sentry.core; + +import io.sentry.core.protocol.Request; +import io.sentry.core.protocol.User; +import io.sentry.core.util.CollectionUtils; +import io.sentry.core.util.Objects; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Removes personal identifiable information from {@link SentryEvent} if {@link + * SentryOptions#isSendDefaultPii()} is set to false. + */ +final class PiiEventProcessor implements EventProcessor { + private static final List SENSITIVE_HEADERS = + Arrays.asList("X-FORWARDED-FOR", "Authorization", "Cookies"); + + private final @NotNull SentryOptions options; + + PiiEventProcessor(final @NotNull SentryOptions options) { + this.options = Objects.requireNonNull(options, "The options object is required"); + } + + @Override + public SentryEvent process(SentryEvent event, @Nullable Object hint) { + if (!options.isSendDefaultPii()) { + final User user = event.getUser(); + if (user != null) { + user.setUsername(null); + user.setIpAddress(null); + user.setEmail(null); + event.setUser(user); + } + final Request request = event.getRequest(); + if (request != null) { + final Map headers = CollectionUtils.shallowCopy(request.getHeaders()); + if (headers != null) { + for (String sensitiveHeader : SENSITIVE_HEADERS) { + headers.remove(sensitiveHeader); + } + request.setHeaders(headers); + } + if (request.getCookies() != null) { + request.setCookies(null); + } + } + } + return event; + } +} diff --git a/sentry-core/src/main/java/io/sentry/core/SentryClient.java b/sentry-core/src/main/java/io/sentry/core/SentryClient.java index e65aac25b..5e7ce3045 100644 --- a/sentry-core/src/main/java/io/sentry/core/SentryClient.java +++ b/sentry-core/src/main/java/io/sentry/core/SentryClient.java @@ -92,6 +92,10 @@ public SentryClient(final @NotNull SentryOptions options, @Nullable Connection c } } + if (event != null) { + event = options.getPiiEventProcessor().process(event, hint); + } + if (event == null) { return SentryId.EMPTY_ID; } diff --git a/sentry-core/src/main/java/io/sentry/core/SentryOptions.java b/sentry-core/src/main/java/io/sentry/core/SentryOptions.java index c3246f8d3..045ace87e 100644 --- a/sentry-core/src/main/java/io/sentry/core/SentryOptions.java +++ b/sentry-core/src/main/java/io/sentry/core/SentryOptions.java @@ -31,6 +31,8 @@ public class SentryOptions { */ private final @NotNull List eventProcessors = new CopyOnWriteArrayList<>(); + private final @NotNull EventProcessor piiEventProcessor; + /** * Code that provides middlewares, bindings or hooks into certain frameworks or environments, * along with code that inserts those bindings and activates them. @@ -210,6 +212,9 @@ public class SentryOptions { /** SdkVersion object that contains the Sentry Client Name and its version */ private @Nullable SdkVersion sdkVersion; + /** whether to send personal identifiable information along with events */ + private boolean sendDefaultPii = false; + /** * Adds an event processor * @@ -996,6 +1001,19 @@ public void setSdkVersion(final @Nullable SdkVersion sdkVersion) { this.sdkVersion = sdkVersion; } + public boolean isSendDefaultPii() { + return sendDefaultPii; + } + + public void setSendDefaultPii(boolean sendDefaultPii) { + this.sendDefaultPii = sendDefaultPii; + } + + @NotNull + public EventProcessor getPiiEventProcessor() { + return piiEventProcessor; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { @@ -1036,6 +1054,8 @@ public SentryOptions() { integrations.add(new ShutdownHookIntegration()); eventProcessors.add(new MainEventProcessor(this)); + // piiEventProcessor is set outside of eventProcessors to make sure that it runs last + piiEventProcessor = new PiiEventProcessor(this); setSentryClientName(BuildConfig.SENTRY_JAVA_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); diff --git a/sentry-core/src/test/java/io/sentry/core/PiiEventProcessorTest.kt b/sentry-core/src/test/java/io/sentry/core/PiiEventProcessorTest.kt new file mode 100644 index 000000000..5c00fc0d5 --- /dev/null +++ b/sentry-core/src/test/java/io/sentry/core/PiiEventProcessorTest.kt @@ -0,0 +1,98 @@ +package io.sentry.core + +import io.sentry.core.protocol.Request +import io.sentry.core.protocol.User +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class PiiEventProcessorTest { + + private class Fixture { + val user = with(User()) { + id = "some-id" + username = "john.doe" + email = "john.doe@example.com" + ipAddress = "66.249.73.223" + this + } + + val request = with(Request()) { + headers = mutableMapOf( + "X-FORWARDED-FOR" to "66.249.73.223", + "Authorization" to "token", + "Cookies" to "some-cookies", + "Safe-Header" to "some-value" + ) + cookies = "some-cookies" + this + } + + fun getSut(sendPii: Boolean) = + PiiEventProcessor(with(SentryOptions()) { + isSendDefaultPii = sendPii + this + }) + } + + private val fixture = Fixture() + + @Test + fun `when sendDefaultPii is set to false, removes user data from events`() { + val eventProcessor = fixture.getSut(sendPii = false) + val event = SentryEvent() + event.user = fixture.user + + eventProcessor.process(event, null) + + assertNotNull(event.user.id) + assertNull(event.user.email) + assertNull(event.user.username) + assertNull(event.user.ipAddress) + } + + @Test + fun `when sendDefaultPii is set to true, does not remove user data from events`() { + val eventProcessor = fixture.getSut(sendPii = true) + val event = SentryEvent() + event.user = fixture.user + + eventProcessor.process(event, null) + + assertNotNull(event.user) + assertNotNull(event.user.id) + assertNotNull(event.user.email) + assertNotNull(event.user.username) + assertNotNull(event.user.ipAddress) + } + + @Test + fun `when sendDefaultPii is set to false, removes user identifiable request headers data from events`() { + val eventProcessor = fixture.getSut(sendPii = false) + val event = SentryEvent() + event.request = fixture.request + + eventProcessor.process(event, null) + + assertNotNull(event.request.headers) + assertNull(event.request.headers["Authorization"]) + assertNull(event.request.headers["X-FORWARDED-FOR"]) + assertNull(event.request.headers["Cookies"]) + assertNotNull(event.request.headers["Safe-Header"]) + } + + @Test + fun `when sendDefaultPii is set to true, does not remove user identifiable request headers data from events`() { + val eventProcessor = fixture.getSut(sendPii = true) + val event = SentryEvent() + event.request = fixture.request + + eventProcessor.process(event, null) + + assertNotNull(event.request.headers) + assertNotNull(event.request.headers["Authorization"]) + assertNotNull(event.request.headers["X-FORWARDED-FOR"]) + assertNotNull(event.request.headers["Cookies"]) + assertNotNull(event.request.headers["Safe-Header"]) + } +} diff --git a/sentry-core/src/test/java/io/sentry/core/SentryClientTest.kt b/sentry-core/src/test/java/io/sentry/core/SentryClientTest.kt index 08cb4531d..c55f8e436 100644 --- a/sentry-core/src/test/java/io/sentry/core/SentryClientTest.kt +++ b/sentry-core/src/test/java/io/sentry/core/SentryClientTest.kt @@ -440,6 +440,21 @@ class SentryClientTest { verify(processor).process(eq(event), anyOrNull()) } + @Test + fun `pii event processor is the last event processor applied`() { + val processor = EventProcessor { event, hint -> + event.user = User() + event.user.email = "john.doe@example.com" + event + } + fixture.sentryOptions.addEventProcessor(processor) + + val event = SentryEvent() + + fixture.getSut().captureEvent(event) + assertNull(event.user.email) + } + @Test fun `when captureSession and no release is set, do nothing`() { fixture.getSut().captureSession(createSession(""))