Fix potential Privilege Escalation via Content Provider (CVE-2018-9492)#2466
Fix potential Privilege Escalation via Content Provider (CVE-2018-9492)#2466markushi merged 8 commits intogetsentry:mainfrom
Conversation
...ava/io/sentry/android/core/internal/util/PrivilegeEscalationViaContentProviderCheckerTest.kt
Outdated
Show resolved
Hide resolved
| * and https://github.com/getsentry/sentry-java/issues/2460 | ||
| */ | ||
| @ApiStatus.Internal | ||
| public final class PrivilegeEscalationViaContentProviderChecker { |
There was a problem hiding this comment.
I created this SRP class to make it testable. Hopefully, Secure Theorem will recognize that the vulnerability is fixed even though I did not 100% copy-paste their suggested code.
There was a problem hiding this comment.
Does it make sense to fetch a snapshot into the product and run the analyser against it?
There was a problem hiding this comment.
@artour-bakiev, I think this is what I'm suggesting in the Next steps section of the PR description. Or do you mean something else?
There was a problem hiding this comment.
I thought about running the datatheorem check before landing the PR into main branch. But perhaps it was intended, sorry if it was :)
| private val fixture = Fixture() | ||
|
|
||
| @Test | ||
| fun `When sdk version is less than vulnerable versions, security check is not performed`() { |
There was a problem hiding this comment.
Minor stuff, it appears the Sentry team is utilising lowercase symbols in their test methods.
There was a problem hiding this comment.
I think they use both upper and lowercase then. I was just following the style they had been using in RootCheckerTest.kt.
I copy-pasted-and-modified their existing RootCheckterTest.kt file for this; https://github.com/getsentry/sentry-java/blob/6.11.0/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/RootCheckerTest.kt#L48
All tests in that file are in the format "When ..., ...."
| } | ||
|
|
||
| public PrivilegeEscalationViaContentProviderChecker( | ||
| final @NotNull BuildInfoProvider buildInfoProvider |
There was a problem hiding this comment.
Minor stuff, final specified for a parameter.
There was a problem hiding this comment.
I was just following the style they had been using in RootChecker.kt.
I copy-pasted-and-modified their existing RootCheckter.kt file for this; https://github.com/getsentry/sentry-java/blob/6.6.0/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/RootChecker.java#L36
Parameters in that file are all final.
Please keep in mind that this code is in Java where parameters are not final and can be reassigned.
In Kotlin, parameters are implicitly final (val) and cannot be reassigned.
I'm guessing that this is why the Sentry team is prepending parameters in Java with final. It is good practice to make sure something is immutable when it does not need to be mutable. Makes everything safer. Kotlin just does these things for us and sometimes we forget that we have to do it ourselves in Java 😁
There was a problem hiding this comment.
All good then, mate. A final param seems to be a bit confusing but it's perhaps just for me.
| @Nullable String s, | ||
| @Nullable String[] strings1, | ||
| @Nullable String s1) { | ||
| new PrivilegeEscalationViaContentProviderChecker().securityCheck(this); |
There was a problem hiding this comment.
It seems we should guard every public (ContentProvider) method.
There was a problem hiding this comment.
I think that the vulnerability is specific to read operations though right? This means we only need to have this in the query method as suggested by Secure Theorem.
I'd like to avoid unnecessary code additions / changes.
There was a problem hiding this comment.
@markushi, it's not necessary to override the other can-be-overloaded query functions because they all ultimately just call this abstract function subclass implementation.
There was a problem hiding this comment.
I think that the vulnerability is specific to read operations though right?
Mmmm, I'm not completely sure since I still can't understand the attack. But according to https://www.cvedetails.com/cve/CVE-2018-9492/ the issue happens because:
In checkGrantUriPermissionLocked of ActivityManagerService.java, there is a possible permissions bypass
It may mean that any public ContentProvider method potentially has the vulnerability (because the checkGrantUriPermissionLocked should happen every time the user addresses a ContentProvider).
There was a problem hiding this comment.
@artour-bakiev, sorry I don't mean to ignore your warnings. From my experience, Data/Secure Theorem typically provides suggested "secure code" samples that are complete. So I'm thinking that guarding query should be enough because that's what they suggested.
There was a problem hiding this comment.
No worries, mate. Let's have a look at the dt scan with v 6.12.0.
There was a problem hiding this comment.
If the DT scan still flags the issue, I will update the Sentry implementation to guard insert, update, and delete functions too.
We might have to wait 2-4 weeks for the next Sentry release but I don't think there is a rush at this point 😄
There was a problem hiding this comment.
Okay, these changes did not pass the Secure/Data Theorem scan...
However, this does not mean that the vulnerability was not actually fixed. Anyways, as agreed upon, I will create another PR to apply the security checks in insert, update, and delete functions.
If it still does not pass the scan, then there is nothing else we can do. It just means that the scanner wants an exact copy-paste of their suggested code 🤷
| * 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. | ||
| */ | ||
| @TargetApi(Build.VERSION_CODES.KITKAT) // Required for ContentProvider#getCallingPackage() |
There was a problem hiding this comment.
Minor, but it seems a bit confusing. It looks like we can safely remove @TargetApi annotation since getCallingPackage() is only called if the device's SDK version is >= 0.
There was a problem hiding this comment.
No because I am using buildInfoProvider.getSdkInfoVersion() to retrieve the sdkVersion. The compiler or lint will complain because we are not using Build.VERSION.SDK_INT directly here.
You can try removing @TargetApi and you will see that you will get a compile or lint error.
I am using buildInfoProvider.getSdkInfoVersion() because it seems to be used throughout the codebase AND it allows me to mock the sdkVersion in the unit tests.
There was a problem hiding this comment.
Since the method is going to be called, you can ignore the linter with the Supress annotation.
There was a problem hiding this comment.
Good point!!! This method can be called even on prior versions to KitKat. Proof is that I'm not doing any version checks at the call sites.
This is embarrassing 😓 I'll suppress instead!
Nice catch, thanks!
| var securityException: SecurityException? = null | ||
| try { | ||
| fixture.getSut().securityCheck(contentProvider) | ||
| } catch (se: SecurityException) { | ||
| securityException = se | ||
| } | ||
|
|
||
| assertNotNull(securityException) |
There was a problem hiding this comment.
Kotlin test has an assertThrows<T> {} function.
There was a problem hiding this comment.
Yep! @markushi pointed out the same thing https://github.com/getsentry/sentry-java/pull/2466/files#r1069057680
markushi
left a comment
There was a problem hiding this comment.
Looks good to me, thanks for your contribution!
Please have a look at my comments.
Regarding testability: I think we should be able to provide you with a public snapshot once the code is merged, so you can create builds in your CI and have them verified.
| @Nullable String s, | ||
| @Nullable String[] strings1, | ||
| @Nullable String s1) { | ||
| new PrivilegeEscalationViaContentProviderChecker().securityCheck(this); |
|
|
||
| contentProvider.mockPackages(null) | ||
|
|
||
| var securityException: SecurityException? = null |
There was a problem hiding this comment.
I think you can simplify this by using assertThrows
| var securityException: SecurityException? = null | |
| assertThrows<SecurityException> { fixture.getSut().securityCheck(contentProvider) } |
There was a problem hiding this comment.
Great callout! I'm an idiot 🤣 I'll change 🔥
There was a problem hiding this comment.
Hmm... Wait a sec... I know I initially tried assertThrows but no code suggestion was popping up for it.
Looking for all instances in java or Kotlin files...
Seems like it requires a dependency currently only used in sentry-spring and sentry-spring-jakarta.
I'd like to avoid changing any build configurations in this PR because I'm not familiar with the current build setup and dependencies. Since I'm only using this try-catch approach in two places in this one file, maybe we can let this slide? If you feel strongly about using assertThrows, you can make changes after the PR has been merged 🙏
| * and https://github.com/getsentry/sentry-java/issues/2460 | ||
| */ | ||
| @ApiStatus.Internal | ||
| public final class PrivilegeEscalationViaContentProviderChecker { |
There was a problem hiding this comment.
It could make sense to give this utility class a more generic name, so we could extends it's functionality in case there will be future CVEs.
| public final class PrivilegeEscalationViaContentProviderChecker { | |
| public final class ContentProviderSecurityChecker { |
There was a problem hiding this comment.
I would typically quote my good ol' friend YAGNI. I don't see future CVEs popping up specifically for ContentProviders when we've been scanning our apps with Secure Theorem for the past 2+ years and this is the only complaint we got so far. Even if something does pop up in the future, at that point we can easily make a refactor if we decide that it belongs in the same file.
However, I do like the generic name you suggested more in terms of length and how less intimidating it is 😃
So, sure! I'll rename to ContentProviderSecurityChecker and also rename the function within to checkPrivilegeEscalation and update the javadocs accordingly 🔥
|
@markushi @marandaneto, I finished addressing all comments. Please let me know what else I need to do 🙏 |
markushi
left a comment
There was a problem hiding this comment.
Thanks again for iterating on this PR! Regarding the exception handling we're already using assertFailsWith<>() from the kotlin test library, which is already a dependency and should work out of the box (as opposed to assertThrows()).
As a last step please run make format in a terminal on the project root folder to ensure all code is formatted.
| import org.mockito.kotlin.mock | ||
| import org.mockito.kotlin.verifyNoInteractions | ||
| import org.mockito.kotlin.whenever | ||
| import kotlin.test.Test |
There was a problem hiding this comment.
| import kotlin.test.Test | |
| import kotlin.test.Test | |
| import kotlin.test.assertFailsWith |
There was a problem hiding this comment.
@markushi, ahh! This is what I've been looking for! Thanks!
I will use assertFailsWith 🔥
| var securityException: SecurityException? = null | ||
| try { | ||
| fixture.getSut().checkPrivilegeEscalation(contentProvider) | ||
| } catch (se: SecurityException) { | ||
| securityException = se | ||
| } | ||
|
|
||
| assertNotNull(securityException) |
There was a problem hiding this comment.
| var securityException: SecurityException? = null | |
| try { | |
| fixture.getSut().checkPrivilegeEscalation(contentProvider) | |
| } catch (se: SecurityException) { | |
| securityException = se | |
| } | |
| assertNotNull(securityException) | |
| assertFailsWith<SecurityException> { | |
| fixture.getSut().checkPrivilegeEscalation(contentProvider) | |
| } |
| var securityException: SecurityException? = null | ||
| try { | ||
| fixture.getSut().checkPrivilegeEscalation(contentProvider) | ||
| } catch (se: SecurityException) { | ||
| securityException = se | ||
| } | ||
|
|
||
| assertNotNull(securityException) |
There was a problem hiding this comment.
| var securityException: SecurityException? = null | |
| try { | |
| fixture.getSut().checkPrivilegeEscalation(contentProvider) | |
| } catch (se: SecurityException) { | |
| securityException = se | |
| } | |
| assertNotNull(securityException) | |
| assertFailsWith<SecurityException> { | |
| fixture.getSut().checkPrivilegeEscalation(contentProvider) | |
| } |
|
Okay, I addressed all PR comments again. I also ran Please review again and hopefully merge if all builds pass 🤞 🙏 |
Codecov ReportBase: 80.14% // Head: 80.14% // No change to project coverage 👍
Additional details and impacted files@@ Coverage Diff @@
## main #2466 +/- ##
=========================================
Coverage 80.14% 80.14%
Complexity 3872 3872
=========================================
Files 312 312
Lines 14669 14669
Branches 1941 1941
=========================================
Hits 11756 11756
Misses 2153 2153
Partials 760 760 Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
| @Nullable String s, | ||
| @Nullable String[] strings1, | ||
| @Nullable String s1) { | ||
| new ContentProviderSecurityChecker().checkPrivilegeEscalation(this); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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" 😀





❗ Problem
Apps that use Secure theorem to scan for security vulnerabilities are being flagged for "Privilege Escalation via Content Provider" (CVE-2018-9492). Devices on Android API 26, 27, and 28 are susceptible to attackers.
The most alarming part of this vulnerability is that it allows attackers to use content providers (even those that do not provide any actual data or handling) to break open the entire system (within or outside the bounds of the application)...
The affected, insecure code are in the
queryfunctions in the following content providers...io.sentry.android.core.SentryInitProviderio.sentry.android.core.SentryPerformanceProviderThe above content providers do not provide any content but may still be used by an attacker.
Users of Sentry Android can work around this issue by removing the content providers and using manual init. However, this would render apps using auto-init vulnerable.
For more info, initial discussion is at #2460
💡 Solution
Apply the suggested "secure code" from secure theorem;
💚 Testing
I added unit tests for the security checker SRP class.
I do not actually know how to perform the attack, so I'm not able to verify that this works against attackers. However, it is safe to assume that it will work given that the attacker will have a different package than the app package.
I also played around with the Android sample to make sure that it still works and not just crashing lol.
📝 Checklist
🔮 Next steps