feat(security): set a restrictive CSP and scope the asset protocol to thumbnails
`app.security.csp` was `null`, so the webview ran with no Content-Security-Policy
at all: any script that reached the web layer would have inherited the whole IPC
surface. There is no known injection path today (one app-owned `{@html}`, no
`innerHTML`/`eval`), so this is defence in depth rather than a fix for an open
hole.
`script-src 'self'` is the restrictive half — Tauri nonces SvelteKit's inline
bootstrap script at build time, so no `'unsafe-inline'` is needed — together with
`object-src`/`frame-src 'none'` and `base-uri 'self'`. `img-src`/`media-src`/
`connect-src` cannot be restrictive: the Jellyfin origin is typed in by the user
at run time and is routinely plain http on a LAN, so they allow `http:`/`https:`.
That is a wide grant for data, but it still bars `file:`/`filesystem:` and does
not touch script execution. A run-time policy naming the server exactly was
rejected: Tauri derives the header from immutable config when it serves the HTML,
so it would mean rebuilding config and reloading the webview on every server
change. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"`
attributes into markup; `worker-src`/`media-src` keep `blob:` for hls.js's
demuxer worker and its MSE object URL; `ipc:`/`http://ipc.localhost` keeps
`invoke` working. `devCsp` mirrors it with the eval/inline/websocket allowances
Vite's dev server needs.
The asset-protocol scope narrows from `$APPDATA/**` — the storage root holding
the SQLite database and the encrypted-token fallback file — to
`$APPDATA/thumbnails/**`. Since DR-137 moved downloaded media to the loopback
media server, `imageCache` is the only `convertFileSrc` caller left.
Needs manual verification on both platforms: thumbnails, online HLS video and
offline downloaded video cannot be exercised headlessly.
This commit is contained in:
@@ -23,11 +23,15 @@ debug = "line-tables-only"
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# protocol-asset serves downloaded media and cached thumbnails to the webview
|
||||
# over http://asset.localhost; without it convertFileSrc yields a URL nothing
|
||||
# answers. Paired with app.security.assetProtocol in tauri.conf.json, which
|
||||
# scopes it to $APPDATA/**.
|
||||
# TRACES: UR-071 | DR-134
|
||||
# protocol-asset serves cached thumbnails to the webview (asset://localhost on
|
||||
# Linux/macOS, http://asset.localhost on Windows/Android); without it
|
||||
# convertFileSrc yields a URL nothing answers. Paired with
|
||||
# app.security.assetProtocol in tauri.conf.json, which scopes it to
|
||||
# $APPDATA/thumbnails/** — the one directory still read through this protocol.
|
||||
# Downloaded media went the same way until DR-137 moved it to the loopback media
|
||||
# server, so the database, the encrypted-token fallback file and downloads/ are
|
||||
# all outside the grant now.
|
||||
# TRACES: UR-012, UR-071 | DR-134, DR-137, DR-198
|
||||
tauri = { version = "2", features = ["protocol-asset"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-os = "2"
|
||||
|
||||
+15
-8
@@ -1029,16 +1029,23 @@ fn set_env_if_unset(key: &str, value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloaded media and cached thumbnails are handed to the webview as
|
||||
/// `http://asset.localhost/…` URLs by `convertFileSrc`. Tauri only answers that
|
||||
/// Cached thumbnails are handed to the webview as asset-protocol URLs by
|
||||
/// `convertFileSrc` (`asset://localhost/…` on Linux/macOS,
|
||||
/// `http://asset.localhost/…` on Windows/Android). Tauri only answers that
|
||||
/// origin when the `protocol-asset` cargo feature is compiled in *and*
|
||||
/// `app.security.assetProtocol.enable` is set in `tauri.conf.json`, which also
|
||||
/// scopes it to `$APPDATA/**` — the storage root holding the database,
|
||||
/// `downloads/` and the thumbnail cache. Both are required together: with either
|
||||
/// missing the URL resolves to nothing and the webview reports
|
||||
/// `NETWORK_NO_SOURCE`, which is how offline video came to fail silently.
|
||||
/// `app.security.assetProtocol.enable` is set in `tauri.conf.json`. Both are
|
||||
/// required together: with either missing the URL resolves to nothing and the
|
||||
/// webview reports `NETWORK_NO_SOURCE`, which is how offline video came to fail
|
||||
/// silently.
|
||||
///
|
||||
/// TRACES: UR-071 | DR-134
|
||||
/// The scope is `$APPDATA/thumbnails/**`, not the storage root: downloaded media
|
||||
/// moved to the loopback media server in DR-137, so `imageCache` is the only
|
||||
/// remaining `convertFileSrc` caller and the database and the encrypted-token
|
||||
/// fallback file — which share that root — never need to be readable by the
|
||||
/// webview. Widen it only if something other than thumbnails starts resolving
|
||||
/// through `convertFileSrc` again.
|
||||
///
|
||||
/// TRACES: UR-012, UR-071 | DR-134, DR-137, DR-198
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Initialize logger
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null,
|
||||
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob: asset: http://asset.localhost http: https:; media-src 'self' blob: asset: http://asset.localhost http://127.0.0.1:* http: https:; connect-src 'self' ipc: http://ipc.localhost http: https:; worker-src 'self' blob:; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'",
|
||||
"devCsp": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob: asset: http://asset.localhost http: https:; media-src 'self' blob: asset: http://asset.localhost http://127.0.0.1:* http: https:; connect-src 'self' ipc: http://ipc.localhost http: https: ws: wss:; worker-src 'self' blob:; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'",
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": ["$APPDATA/**"]
|
||||
"scope": ["$APPDATA/thumbnails/**"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user