-
-
Notifications
You must be signed in to change notification settings - Fork 448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix potential Privilege Escalation via Content Provider (CVE-2018-9492) #2466
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to fetch a snapshot into the product and run the analyser against it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor stuff, it appears the Sentry team is utilising lowercase symbols in their test methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor stuff, final
specified for a parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All good then, mate. A final
param seems to be a bit confusing but it's perhaps just for me.
@@ -51,6 +53,7 @@ public void attachInfo(@NotNull Context context, @NotNull ProviderInfo info) { | |||
@Nullable String s, | |||
@Nullable String[] strings1, | |||
@Nullable String s1) { | |||
new PrivilegeEscalationViaContentProviderChecker().securityCheck(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems we should guard every public (ContentProvider
) method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries, mate. Let's have a look at the dt scan with v 6.12.0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created another PR; #2482
* 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the method is going to be called, you can ignore the linter with the Supress annotation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! d8272e2
var securityException: SecurityException? = null | ||
try { | ||
fixture.getSut().securityCheck(contentProvider) | ||
} catch (se: SecurityException) { | ||
securityException = se | ||
} | ||
|
||
assertNotNull(securityException) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kotlin test has an assertThrows<T> {}
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep! @markushi pointed out the same thing https://github.com/getsentry/sentry-java/pull/2466/files#r1069057680
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -51,6 +53,7 @@ public void attachInfo(@NotNull Context context, @NotNull ProviderInfo info) { | |||
@Nullable String s, | |||
@Nullable String[] strings1, | |||
@Nullable String s1) { | |||
new PrivilegeEscalationViaContentProviderChecker().securityCheck(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
contentProvider.mockPackages(null) | ||
|
||
var securityException: SecurityException? = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great callout! I'm an idiot 🤣 I'll change 🔥
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 🔥
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! 1752102
@markushi @marandaneto, I finished addressing all comments. Please let me know what else I need to do 🙏 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import kotlin.test.Test | |
import kotlin.test.Test | |
import kotlin.test.assertFailsWith |
There was a problem hiding this comment.
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
🔥
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! b7a40d7
var securityException: SecurityException? = null | ||
try { | ||
fixture.getSut().checkPrivilegeEscalation(contentProvider) | ||
} catch (se: SecurityException) { | ||
securityException = se | ||
} | ||
|
||
assertNotNull(securityException) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var securityException: SecurityException? = null | |
try { | |
fixture.getSut().checkPrivilegeEscalation(contentProvider) | |
} catch (se: SecurityException) { | |
securityException = se | |
} | |
assertNotNull(securityException) | |
assertFailsWith<SecurityException> { | |
fixture.getSut().checkPrivilegeEscalation(contentProvider) | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! b7a40d7
var securityException: SecurityException? = null | ||
try { | ||
fixture.getSut().checkPrivilegeEscalation(contentProvider) | ||
} catch (se: SecurityException) { | ||
securityException = se | ||
} | ||
|
||
assertNotNull(securityException) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var securityException: SecurityException? = null | |
try { | |
fixture.getSut().checkPrivilegeEscalation(contentProvider) | |
} catch (se: SecurityException) { | |
securityException = se | |
} | |
assertNotNull(securityException) | |
assertFailsWith<SecurityException> { | |
fixture.getSut().checkPrivilegeEscalation(contentProvider) | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! b7a40d7
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. |
@@ -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); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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" 😀
❗ 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
query
functions in the following content providers...io.sentry.android.core.SentryInitProvider
io.sentry.android.core.SentryPerformanceProvider
The 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