Skip to content

Add in-app HTML viewer with sandboxed JS execution#77

Merged
jsgrrchg merged 4 commits into
jsgrrchg:mainfrom
spamsch:feat/in-app-html-viewer
May 11, 2026
Merged

Add in-app HTML viewer with sandboxed JS execution#77
jsgrrchg merged 4 commits into
jsgrrchg:mainfrom
spamsch:feat/in-app-html-viewer

Conversation

@spamsch
Copy link
Copy Markdown
Contributor

@spamsch spamsch commented May 11, 2026

Summary

Adds a dedicated in-app viewer for .html / .htm files so they render as a page (with JS) instead of falling through to the text/code viewer.

End-to-end:

  • crates/vault/src/vault.rsclassify_vault_entry_path now returns viewer_kind: "html" for .html and .htm, ahead of the generic text fallback.
  • apps/desktop/src-electron/main/ipc.tsregisterPreviewProtocolHandler gains an assets scope: neverwrite-file://localhost/assets/<base64-vault>/<raw/relative/path>. The trailing segments stay un-base64 so that subresources referenced from the HTML (CSS, images, ./data.json) resolve naturally via relative URLs. Path validation still goes through resolvePreviewFilePath, so traversal is rejected at the main process.
  • apps/desktop/src/features/editor/HtmlTabView.tsx — new React view. Hosts the document in an <iframe> with sandbox="allow-scripts allow-same-origin allow-forms allow-modals" and referrerPolicy="no-referrer". Header reuses the local pattern from FileTabView for Open Externally / Reveal in Finder.
  • apps/desktop/src/features/editor/FileTabView.tsx — routes viewer === "html" to HtmlTabView.
  • apps/desktop/src/app/store/editorTabs.tsFileViewerMode gains "html", inferFileViewer maps .html/.htm, and fileViewerNeedsTextContent returns false for "html" so the file is not pre-fetched as text.
  • apps/desktop/src/app/utils/vaultEntries.ts — adds a short-circuit alongside the existing image short-circuit so html entries open without a text read.
  • apps/desktop/src/app/utils/filePreviewUrl.tsbuildVaultAssetUrl helper.
  • apps/desktop/config/desktop-security.json — renderer CSP adds neverwrite-file: to frame-src so the parent can embed the iframe. CSP test updated.

Security boundary

  • Iframe origin is neverwrite-file://localhost (because of allow-same-origin), different from the renderer's origin. Scripts in the iframe cannot script the parent app, read parent storage/cookies, or escape into the renderer process.
  • Iframe scripts CAN fetch other neverwrite-file://localhost/... URLs (same-scheme/host). Every such request goes through resolvePreviewFilePath, which validates the path against the active vault root, so a script cannot reach files outside the vault.
  • To stop a malicious local document from POSTing vault contents to an external server, the protocol handler attaches a Content-Security-Policy response header to HTML responses that pins connect-src and frame-src to 'self' neverwrite-file: and sets form-action 'none'. Local self-contained pages (including ones doing fetch("./sibling.json")) keep working; arbitrary outbound network is blocked.
  • Pages that legitimately need full network access can still be opened in the system browser via the existing Open Externally button.

Test plan

  • Drop a .html file containing inline <script> into a vault; opening it from the file tree renders the page in-app and the script runs.
  • In the page devtools console, fetch("https://example.com") is refused (CSP violation).
  • fetch("./sibling.json") against a real sibling file succeeds.
  • Open Externally button hands the file off to the system browser.
  • vitest run src/app/utils/desktopCsp.test.ts passes.
  • cargo test -p neverwrite-vault passes.

Vault html/htm files now classify with viewer_kind "html" and open in
a dedicated React view that hosts the document in an iframe loaded
through a new "assets" scope on the existing neverwrite-file:
protocol. The new scope takes the raw relative path as the trailing
segments so that subresources (CSS, images, sibling .json) referenced
from the HTML resolve naturally; path validation still goes through
resolvePreviewFilePath so traversal is rejected.

Scripts execute (sandbox="allow-scripts allow-same-origin allow-forms
allow-modals"). To stop a malicious local document from exfiltrating
vault contents, the protocol handler attaches a Content-Security-Policy
response header that restricts connect-src and frame-src to
neverwrite-file: only, leaving local fetch("./data.json") working but
blocking arbitrary outbound network. Pages that need full network
access can still be opened in the system browser from the header.

The renderer CSP also adds neverwrite-file: to frame-src so the parent
can embed the iframe at all. Html file entries skip the text content
pre-fetch since the iframe loads them directly.
@jsgrrchg
Copy link
Copy Markdown
Owner

This is a really great idea, thank you so much. I didn’t realize how useful it could be! Also, thank you for taking care of the security aspects of this feature and thinking through the whole logic.

I’m still stuck with an older PR and having a hard time with the Linux release pipeline, this will be merged as soon as possible.

Thanks again!

@jsgrrchg jsgrrchg merged commit 87be720 into jsgrrchg:main May 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants