Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Use a single TransactionPerfomanceCollector ([#2464](https://github.com/getsentry/sentry-java/pull/2464))
- Don't override sdk name with Timber ([#2450](https://github.com/getsentry/sentry-java/pull/2450))
- Set transactionNameSource to CUSTOM when setting transaction name ([#2405](https://github.com/getsentry/sentry-java/pull/2405))
- Guard against CVE-2018-9492 "Privilege Escalation via Content Provider" ([#2466](https://github.com/getsentry/sentry-java/pull/2466))

## 6.11.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.net.Uri;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.android.core.internal.util.ContentProviderSecurityChecker;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -51,6 +52,7 @@ public void attachInfo(@NotNull Context context, @NotNull ProviderInfo info) {
@Nullable String s,
@Nullable String[] strings1,
@Nullable String s1) {
new ContentProviderSecurityChecker().checkPrivilegeEscalation(this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could we just create an instance and keep it as a class member instead of creating a new instance of the SecurityChecker every time the method is called? Same for SentryPerformanceProvider.

Copy link
Contributor Author

@vestrel00 vestrel00 Jan 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could. It's what I would have done if the call-sites were using dependency injection or if we had to use an instance of this class in at least one other function. It's not used anywhere else, nor is this function really invoked (it's only invoke if an attacker tries the exploit) so performance shouldn't be an issue.

I was originally writing a static function but ended up with an SRP class instead for testability and future-proofing in case it needs to be mocked at call-sites.

I'll ignore this since this is a "nit" 😀

return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import android.os.Bundle;
import android.os.SystemClock;
import io.sentry.DateUtils;
import io.sentry.android.core.internal.util.ContentProviderSecurityChecker;
import java.util.Date;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -78,6 +79,7 @@ public Cursor query(
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
new ContentProviderSecurityChecker().checkPrivilegeEscalation(this);
return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.sentry.android.core.internal.util;

import android.annotation.SuppressLint;
import android.content.ContentProvider;
import android.net.Uri;
import android.os.Build;
import io.sentry.NoOpLogger;
import io.sentry.android.core.BuildInfoProvider;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public final class ContentProviderSecurityChecker {

private final @NotNull BuildInfoProvider buildInfoProvider;

public ContentProviderSecurityChecker() {
this(new BuildInfoProvider(NoOpLogger.getInstance()));
}

public ContentProviderSecurityChecker(final @NotNull BuildInfoProvider buildInfoProvider) {
this.buildInfoProvider = buildInfoProvider;
}

/**
* Protects against "Privilege Escalation via Content Provider" (CVE-2018-9492).
*
* <p>Throws a SecurityException if the security check is breached.
*
* <p>See https://www.cvedetails.com/cve/CVE-2018-9492/ and
* https://github.com/getsentry/sentry-java/issues/2460
*
* <p>Call this function in the {@link ContentProvider#query(Uri, String[], String, String[],
* String)} function.
*
* <p>This should be invoked regardless of whether there is data to query or not. The attack is
* not contained to the specific provider but rather the entire system.
*
* <p>This blocks the attacker by only allowing the app itself (not other apps) to "query" the
* provider.
*
* <p>The vulnerability is specific to un-patched versions of Android 8 and 9 (API 26 to 28).
* Therefore, this security check is limited to those versions to mitigate risk of regression.
*/
@SuppressLint("NewApi")
public void checkPrivilegeEscalation(ContentProvider contentProvider) {
final int sdkVersion = buildInfoProvider.getSdkInfoVersion();
if (sdkVersion >= Build.VERSION_CODES.O && sdkVersion <= Build.VERSION_CODES.P) {

String callingPackage = contentProvider.getCallingPackage();
String appPackage = contentProvider.getContext().getPackageName();
if (callingPackage != null && callingPackage.equals(appPackage)) {
return;
}
throw new SecurityException("Provider does not allow for granting of Uri permissions");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.sentry.android.core.internal.util

import android.content.ContentProvider
import android.content.Context
import android.os.Build
import io.sentry.android.core.BuildInfoProvider
import org.mockito.kotlin.mock
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever
import kotlin.test.Test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import kotlin.test.Test
import kotlin.test.Test
import kotlin.test.assertFailsWith

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markushi, ahh! This is what I've been looking for! Thanks!

I will use assertFailsWith 🔥

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! b7a40d7

import kotlin.test.assertFailsWith

class ContentProviderSecurityCheckerTest {

private class Fixture {
val buildInfoProvider = mock<BuildInfoProvider>()

fun getSut(
sdkVersion: Int = Build.VERSION_CODES.O
): ContentProviderSecurityChecker {
whenever(buildInfoProvider.sdkInfoVersion).thenReturn(sdkVersion)

return ContentProviderSecurityChecker(
buildInfoProvider
)
}
}

private val fixture = Fixture()

@Test
fun `When sdk version is less than vulnerable versions, security check is not performed`() {
val contentProvider = mock<ContentProvider>()

fixture.getSut(Build.VERSION_CODES.N_MR1).checkPrivilegeEscalation(contentProvider)

verifyNoInteractions(contentProvider)
}

@Test
fun `When sdk version is greater than vulnerable versions, security check is not performed`() {
val contentProvider = mock<ContentProvider>()

fixture.getSut(Build.VERSION_CODES.Q).checkPrivilegeEscalation(contentProvider)

verifyNoInteractions(contentProvider)
}

@Test
fun `When calling package is null, security check exception is thrown`() {
val contentProvider = mock<ContentProvider>()

contentProvider.mockPackages(null)

assertFailsWith<SecurityException> {
fixture.getSut().checkPrivilegeEscalation(contentProvider)
}
}

@Test
fun `When calling package does not match app package, security check exception is thrown`() {
val contentProvider = mock<ContentProvider>()

contentProvider.mockPackages("{$APP_PACKAGE}.attacker")

assertFailsWith<SecurityException> {
fixture.getSut().checkPrivilegeEscalation(contentProvider)
}
}

@Test
fun `When calling package matches app package, no security exception thrown`() {
val contentProvider = mock<ContentProvider>()

contentProvider.mockPackages(APP_PACKAGE)

fixture.getSut().checkPrivilegeEscalation(contentProvider)

// No exception!
}
}

private fun ContentProvider.mockPackages(callingPackage: String?) {
whenever(this.callingPackage).thenReturn(callingPackage)

val context = mock<Context>()
whenever(this.context).thenReturn(context)
whenever(context.packageName).thenReturn(APP_PACKAGE)
}

private const val APP_PACKAGE = "com.app"