fix(web): isolate untrusted remote content - #6545
Conversation
Restrict wallet bridges to the reviewed HTTPS origin and block mixed content. Reject unsafe app-card cover targets and avoid fetches during save or share. Leave the app-wide cleartext network policy unchanged.
There was a problem hiding this comment.
Pull request overview
Hardens embedded web content and app-card media handling against untrusted origins and network targets.
Changes:
- Restricts injectable WebViews to HTTPS origins and blocks mixed content.
- Validates app-card cover URLs before display.
- Makes app-card save/share operations cache-only.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
app/src/test/java/one/mixin/android/vo/AppCardDataTest.kt |
Adds cover URL validation tests. |
app/src/main/java/one/mixin/android/vo/AppCardData.kt |
Introduces app-card image URL filtering. |
app/src/main/java/one/mixin/android/ui/web/WebFragment.kt |
Adds WebView origin restrictions and bridge gating. |
app/src/main/java/one/mixin/android/ui/media/pager/MediaPagerActivity.kt |
Resolves app-card images only from disk cache. |
app/src/main/java/one/mixin/android/ui/conversation/holder/AppCard.kt |
Filters cover URLs before rendering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Filter every DNS result and verify the connected route for image requests, including redirects. Disable proxies for the image client so target resolution cannot bypass the public-address policy. Cover IPv6 ULA and reserved ranges.
Block untrusted subframe requests without launching the system browser. Track redirects and external navigation only for main-frame requests.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (3)
app/src/main/java/one/mixin/android/util/image/ImageNetworkPolicy.kt:66
- This condition blocks the entire
192.0.0.0/16, not just the non-public special-purpose ranges inside it. Globally routable hosts in that /16 (for example,192.0.66.0/24) will therefore fail every Coil image request now that this policy is installed app-wide. Narrow this check to the actual reserved prefixes (and preserve the globally reachable exceptions in192.0.0.0/24).
bytes[0] == 192 && bytes[1] == 0 ||
app/src/main/java/one/mixin/android/util/image/ImageNetworkPolicy.kt:39
- The HTTPS check is only applied to the original app-card URL. OkHttp follows HTTPS-to-HTTP redirects by default, and this interceptor validates only the connected address, so an accepted cover can still be fetched over cleartext after a redirect. Revalidate the scheme on redirect requests (without blocking unrelated direct HTTP image loads, which the PR says remain supported).
private object PublicImageNetworkInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val address = chain.connection()?.route()?.socketAddress?.address
if (address == null || address.isBlockedImageAddress()) {
throw IOException("Blocked non-public image target")
}
return chain.proceed(chain.request())
app/src/main/java/one/mixin/android/ui/media/pager/MediaPagerActivity.kt:471
- This returns the disk-cache backing file after
usehas closed the snapshot. Once the snapshot is closed, Coil may evict or replace that entry; save copies it shortly afterward, but sharing hands the URI to another app that may open it much later, so the share can fail if the cache changes. Copy the cached bytes to a stable temporary file while the snapshot is open and return that copy.
imageLoader.diskCache?.openSnapshot(coverUrl)?.use { snapshot ->
snapshot.data.toFile()
}
| }, | ||
| ), | ||
| "_mw_", | ||
| ) |
…sted-web-content # Conflicts: # app/src/main/java/one/mixin/android/ui/conversation/holder/AppCard.kt
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
app/src/main/java/one/mixin/android/ui/web/WebFragment.kt:995
addJavascriptInterfaceexposes these objects to every frame, not just the trusted top-level origin. The URL gate does not close that boundary: Android does not callshouldOverrideUrlLoadingfor POST navigations, so a trusted page can submit a cross-origin form into an iframe and that untrusted frame receivesMixinContextand_mw_. Use an origin-scoped messaging mechanism (or otherwise ensure untrusted child documents cannot receive wallet capabilities) rather than relying on navigation interception around a frame-global bridge.
if (injectable) {
webAppInterface?.let { webView.addJavascriptInterface(it, "MixinContext") }
webView.addJavascriptInterface(
Web3Interface(
app/src/main/java/one/mixin/android/util/image/ImageNetworkPolicy.kt:67
- This condition blocks the entire
192.0.0.0/16, although the special-purpose blocks here are narrower (for example,192.0.0.0/24and192.0.2.0/24). Because this policy is installed on the app-wide Coil loader, otherwise valid public images whose DNS resolves to addresses such as192.0.1.xare rejected as private targets. Narrow this check to the actual non-public prefixes rather than filtering the full/16.
bytes[0] == 0 ||
bytes[0] == 100 && bytes[1] in 64..127 ||
bytes[0] == 192 && bytes[1] == 0 ||
bytes[0] == 198 && bytes[1] in 18..19 ||
app/src/main/java/one/mixin/android/ui/media/pager/MediaPagerActivity.kt:470
usecloses the Coil snapshot before the returned cache file is consumed. Once closed, the entry is eligible for update or LRU eviction;savereads it only after this function returns, and sharing hands the URI to another app that may open it later. A concurrent cache write/trim can therefore delete the file and make save/share fail. Copy the data to a stable temporary file while the snapshot is open (and manage that file's cleanup) instead of returning the cache-owned path.
imageLoader.diskCache?.openSnapshot(coverUrl)?.use { snapshot ->
snapshot.data.toFile()
app/src/main/java/one/mixin/android/util/image/ImageNetworkPolicy.kt:39
- The interceptor validates only the connected address, so an app-card cover that passes the initial HTTPS check can still follow OkHttp's default HTTPS-to-HTTP redirect and fetch over cleartext. Redirect follow-ups pass through here without a scheme check, while this app explicitly permits cleartext traffic. Preserve the app-wide HTTP policy for unrelated images, but ensure app-card requests reject cross-scheme redirects (for example via a dedicated/tagged request policy).
val address = chain.connection()?.route()?.socketAddress?.address
if (address == null || address.isBlockedImageAddress()) {
throw IOException("Blocked non-public image target")
}
return chain.proceed(chain.request())
| bytes[0] == 100 && bytes[1] in 64..127 || | ||
| bytes[0] == 192 && bytes[1] == 0 || | ||
| bytes[0] == 198 && bytes[1] in 18..19 || | ||
| bytes[0] >= 224 |
What changed
Why
Wallet bridges could remain available after cross-origin navigation, and the asset-origin check always evaluated true. App-card cover URLs could also trigger requests to local or private targets during rendering or save/share.
Impact
Cross-origin links open in the system browser. Unsafe app-card covers are ignored, and uncached covers cannot be saved or shared. The existing app-wide cleartext/HTTP network policy is unchanged.
Validation
./gradlew --no-configuration-cache :app:testGooglePlayDebugUnitTest --tests one.mixin.android.vo.AppCardDataTest./gradlew --no-configuration-cache :app:compileGooglePlayDebugKotlin