Skip to content

emrg: restrict main-window file:// navigation to renderer dist (fixes #1100) - #1101

Merged
argszero merged 1 commit into
masterfrom
feature/gui-nav-file-hardening
Sep 1, 2026
Merged

emrg: restrict main-window file:// navigation to renderer dist (fixes #1100)#1101
argszero merged 1 commit into
masterfrom
feature/gui-nav-file-hardening

Conversation

@argszero

@argszero argszero commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

Hardening follow-up to #1097 (v0.2.91). externalNavPolicy() previously allowed all file:// URLs into the privileged main window; any local HTML file could be loaded with the full window.emrg bridge (53 invoke + onEvent, daemon control) — a local-file privilege escalation vector.

This PR narrows the "allow" rule to only the app's own renderer dist file URLs:

  • emrg/gui/nav-policy.jsexternalNavPolicy(url, rendererDistUrl) now:
    • "allow" only for file:// URLs inside the renderer-dist prefix (loadFile / reload / SPA self-navigation)
    • "deny" for every other file:// URL (fail-closed; no prefix provided → all file:// denied)
    • "open-external" unchanged for http/https/other schemes
    • URLs are canonicalized through new URL() (WHATWG) before prefix matching, so dist/../index.html, dist/%2e%2e/… and file://localhost/… cannot bypass the prefix via path traversal
  • emrg/gui/main.js — passes a RENDERER_DIST_URL prefix (derived from pathToFileURL(…/renderer/dist/…).href + "/") at both call sites (will-navigate + setWindowOpenHandler)
  • emrg/gui/test/nav-policy.test.js — positive cases (dist assets, SPA self-nav) + negative cases (arbitrary local files, dist-evil prefix boundary, ../%2e%2e traversal, fail-closed without prefix)

Verification

  • GUI node suite: cd emrg/gui && npm test93 passed, 8 skipped, 0 failed (nav-policy tests 4 → 7)
  • Python: uv run pytest tests/1212 passed, 1 skipped (unchanged)
  • node --check on all touched JS files passes

Fixes #1100

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260902-021857 (distinct from authoring cycle 013230)

Reviewed the full diff (3 files, +73/−18) and re-ran the branch tests:

  • nav-policy.js: externalNavPolicy(url, rendererDistUrl) is now fail-closed — only renderer-dist-prefixed file:// URLs get "allow"; every other file:// returns "deny" (no prefix passed → all denied). WHATWG new URL() canonicalization before prefix matching blocks .. / %2e%2e traversal and file://localhost variants (verified: dist/../index.html normalizes outside the prefix → deny). Trailing / on the prefix prevents dist-evil boundary matching.
  • main.js: RENDERER_DIST_URL = pathToFileURL(…/renderer/dist).href + "/" passed at BOTH call sites (will-navigate + setWindowOpenHandler); non-dist file:// in setWindowOpenHandler is denied with no window opened.
  • Tests: 7/7 pass locally (positive dist cases + negative arbitrary-file/prefix-boundary/traversal/fail-closed cases) — both positive and negative states verified.
  • CI: test 33538822579 SUCCESS (test + test-windows); PR MERGEABLE / CLEAN.

Security posture intact: only the app's own built renderer assets can load in the privileged main window. Good hardening of #1097.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260902-022654 (distinct from authoring 013230 and LGTM-1/3 021857)

Re-verified head 8f15d9c unchanged and CI still green (test 33538822579 pass: test + test-windows). Prior adversarial review stands: fail-closed nav policy (only renderer-dist file:// allowed, all other file:// denied, WHATWG URL canonicalization blocks traversal/encoded bypasses), prefix passed at both call sites, 7/7 tests cover positive + negative states. MERGEABLE / CLEAN. 2/3.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260902-023333 (distinct from authoring 013230, LGTM-1/3 021857, LGTM-2/3 022654)

Head 8f15d9c unchanged, CI double-green (test 33538822579 pass). Fresh local re-run of branch tests: 7/7 pass. MERGEABLE / CLEAN. Prior adversarial reviews (fail-closed dist-prefix nav policy, WHATWG URL canonicalization, both call sites wired, positive + negative test coverage) remain valid. 3/3 — ready to merge.

@argszero
argszero merged commit dbdd862 into master Sep 1, 2026
2 checks passed
@pm25coder

Copy link
Copy Markdown
Collaborator

I tested this hardening on the Windows host at the merged master head (dbdd862) and verified it end-to-end:

  • fail-closed logic confirmed: externalNavPolicy(url, rendererDistUrl) returns "deny" for every file:// URL when no dist prefix is provided; with a prefix, URLs are canonicalized through new URL() (WHATWG) before prefix matching — ../%2e%2e dot-segments collapse at parse time and file://localhost/... normalizes to file:///..., so dist/../../... string-prefix bypasses are structurally impossible.
  • Both call sites wired: RENDERER_DIST_URL is derived from pathToFileURL(path.join(__dirname, "renderer", "dist")).href + "/" and passed to both will-navigate and setWindowOpenHandler in main.js — the trailing / gives a boundary-safe prefix (a dist-evil sibling directory cannot match).
  • GUI node suite: 101 tests / 93 pass / 8 skip (integration) — nav-policy now 7 tests (90-96): renderer-dist allow, outside-prefix deny, prefix-boundary/traversal/encoded variants, fail-closed without prefix, http(s) open-external, non-file schemes, empty/non-string deny.
  • Python side: pytest collects 1213 (= 1212 passed + 1 skipped), import emrg = 0.2.91, CLI OK.
  • Post-merge Test CI (33544410263) SUCCESS on dbdd862.

Non-blocking: the secondary item in issue #1100 (Composer draft writes to the shared transcript store per keystroke — React.memo on TranscriptView or a separate draft version counter would keep typing cost O(1)) is still open; worth a follow-up when convenient.

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.

gui: externalNavPolicy allows all file:// URLs into the privileged main window (hardening after #1097)

2 participants