shell: Enforce script network policy for TextView images - #3230
Conversation
GPUI renders an SVG <image> whose href is not a data URL from the local file it names, so an SVG fetched under a GET grant could draw files the script has no grant to read. Parse the SVG with a resolver that records such an href before rendering, and refuse the image when it has one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
huacnlee
left a comment
There was a problem hiding this comment.
Thanks! Two notes:
-
I pushed fe5ad9a to close a local-file read through SVG images. GPUI's
SvgRendereruses usvg's default image resolver, which reads an<image href>that is not a data URL from disk (absolute paths as written, relative ones from the working directory). An SVG served from a granted host could therefore draw local files the script has no grant to read. Decoding now parses the SVG first with a resolver that records such hrefs, and refuses the image when it has one. A test covers it. -
Since the image cache is now scoped to the view's keyed state, a TextView that is not drawn for a frame (scrolled out of a list, conditionally hidden) fetches its images again when it reappears. That is the right trade for isolating grants, and the requests go through the host's
HttpClient, so a host client that honors HTTPCache-Controlavoids the network round trip; no change needed here.
# Conflicts: # crates/base/src/text/text_view.rs
Description
Script-created HTML and Markdown TextViews currently fetch document images with the host's HTTP client without checking the creating script's network capabilities. This includes requests made to measure intrinsic image sizes and table columns.
Capture the creating Policy in the TextView specification and return a GPUI
ImageSource::Customfor every document image. A thin element wrapper initializes the document owner duringrequest_layout, so constructing a TextView during CLIcheckneeds no active drawing context. A privateAssetimplementation uses GPUI's existing asynchronous loading, cache and redraw notifications; its key includes the document owner, isolating different views and policies. Authorize GET requests withCapabilities::may_request, disable automatic redirects and re-authorize each hop. Decode the fetched bytes without passing the original URL back to GPUI. Loads run in the background with a 30-second timeout, an 8 MiB body limit and at most 10 redirects; HTTPS downgrades are refused. Dropping the view cancels pending loads and releases decoded images.Base gains a narrow image-source override covering inline/block images and measurement, including embedded data URLs. Shell routes every document image through that asset loader and rejects non-HTTP(S), relative and credential-bearing URLs. Failure or pending loads cannot fall back to the host loader. Native TextViews without an override keep their existing behavior. This PR only addresses document image loading.
Public API
gpui-base
Override every document image source for rendering and intrinsic-size measurement; the returned source is authoritative, including when loading fails or remains pending.
Breaking Changes
Shell
TextView.html(id, text)andTextView.markdown(id, text)retain their signatures, but remote images now require the creating script's GET grant. Non-HTTP(S) document images (includingdata:) are refused. For an image athttps://images.example/image.png, embedders must explicitly grant it:Every redirect target also needs authorization. The application-asset
image(path)API is unaffected.How to Test
The image tests intercept the actual HTTP transport used by this implementation and assert
RedirectPolicy::NoFollow. They cover positive PNG loading and decoding, denied initial requests, scheme/host/path/port/method grants, authorized and denied redirects, downgrade refusal, response limits, concurrent policies, HTML/Markdown, inline/block/table images, intrinsic measurement, repeated frames and embedded data rejection. A separate regression test callsShellRuntime::checkwith plain HTML and Markdown TextViews outside drawing; it reproduced the lifecycle panic before the fix.cargo test -p gpui-shell --lib --locked -- --skip tests::standard_runtime --skip tests::network --skip tests::fs --skip tests::process --skip tests::benchmark: 663 passed (same exclusions as the runtime-core CI job).cargo test -p gpui-base --lib text:: --locked: 250 passed.cargo clippy -p gpui-shell --all-targets --locked -- -D warnings: passed.cargo fmt --all -- --checkandgit diff --check: passed.cargo run -p gpui-base-examples --bin components --locked -- text-view: built and launched on Linux (startup smoke check only). No manual interaction, screenshot validation or cross-platform performance profiling was completed. Rendering and measurement coverage comes from the GPUI visual tests above.npm --prefix crates/component-shell/tests/types ciandnpm --prefix crates/component-shell/tests/types test: passed, including the CLIcheckthat previously panicked.Checklist
cargo runfor story tests related to the changes.