From 2d21f092d564b3fbfcfbc7e0ad00f437c68184c0 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 16 Aug 2026 22:59:47 +0200 Subject: [PATCH] fix(android): stop the webview undoing the network security config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainActivity set mixedContentMode = MIXED_CONTENT_ALWAYS_ALLOW together with allowFileAccess/allowContentAccess = true, which is a blanket cleartext opt-in reached by hand — the exact thing network_security_config.xml exists to prevent and its own comment warns against. Nothing needed any of the three: - file:// is never loaded. Cached thumbnails go through convertFileSrc, which on Android resolves to http://asset.localhost/... and is answered by wry's request interceptor rather than the filesystem; downloaded media goes over the loopback HTTP server (DR-137), which exists precisely because the asset/file route cannot stream a large file. - content:// is never loaded. The manifest's FileProvider is for outbound share intents, not webview navigation. - Mixed content never arises. Tauri serves the UI from http://tauri.localhost (use_https_scheme defaults false and is not set), and both 127.0.0.1 and asset.localhost are loopback/.localhost origins Chromium treats as potentially trustworthy. A plain-HTTP remote server would be mixed content, but the network security config already rejects it first — so ALWAYS_ALLOW bought nothing. COMPATIBILITY_MODE rather than NEVER_ALLOW is a deliberate hedge: the platform default at targetSdk 21+ is NEVER_ALLOW, so this is still one step looser, and it keeps passive content working if the analysis missed a path. The two files now cross-reference each other so the pair cannot drift apart again. Also records why POST_NOTIFICATIONS is declared but never requested. An audit read the missing runtime request as a threat to the lockscreen controls; it is not. A foreground-service notification is explicitly NOT exempt, but a media-session one is, and the platform predicate (Notification.isMediaNotification) requires MediaStyle AND a non-null session token. Confirmed on device: appops POST_NOTIFICATION: ignore with the transport notification live. So no permission prompt is added and startForeground stays ungated — a guard there would trade a cosmetic problem for the "did not then call Service.startForeground()" kill. What is added is the guard matching the real precondition: both builders bind the token once and log an error if it is ever null, since SystemUI's media carousel is gated on the same predicate and a token-less notification loses the lockscreen controls entirely, silently. TRACES: UR-006, UR-071 | DR-198, DR-199 --- docs/architecture/09-security.md | 2 + docs/requirements.md | 6 +- scripts/extract-traces.test.ts | 6 +- .../android/src/main/AndroidManifest.xml | 29 ++++ .../com/dtourolle/jellytau/MainActivity.kt | 49 ++++++- .../player/JellyTauPlaybackService.kt | 127 +++++++++++++++++- .../main/res/xml/network_security_config.xml | 7 +- 7 files changed, 216 insertions(+), 10 deletions(-) diff --git a/docs/architecture/09-security.md b/docs/architecture/09-security.md index b54309ca..5faf88d2 100644 --- a/docs/architecture/09-security.md +++ b/docs/architecture/09-security.md @@ -50,6 +50,8 @@ pub struct EncryptedFileStorage; // AES-256-GCM fallback | Certificate Validation | System CA store (configurable for self-signed) | | Token Transmission | Bearer token in `Authorization` header only | | Token Refresh | Handled by Jellyfin server (long-lived tokens) | +| Android cleartext | `res/xml/network_security_config.xml` blocks cleartext everywhere except `127.0.0.1` (the loopback media server, DR-137/DR-138). The manifest's `usesCleartextTraffic` is ignored once the config is present, so the config is the single authority | +| Android WebView | `mixedContentMode = COMPATIBILITY` with `allowFileAccess`/`allowContentAccess` both `false` (DR-199). These are the second half of the cleartext policy: `ALWAYS_ALLOW` re-opened by hand what the network security config closes. Change the two together | ## Local Data Protection diff --git a/docs/requirements.md b/docs/requirements.md index 5a5d498d..65b07605 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -352,6 +352,8 @@ Internal architecture, components, and application logic. | DR-195 | Play/pause works on the native path, because the frontend stops claiming a webview element is playing when there is none. `html5_playing` is Rust's record of "a webview `