Skip to content

Commit 20ea57c

Browse files
Stvadclaude
andcommitted
fix(qa): scope vite-qa fs allowlist instead of disabling strict
The worktree dev server is reached over the Tailscale tunnel during iPad testing (VITE_TUNNEL), not just localhost. `server.fs.strict: false` made Vite serve ANY local file over `/@fs/...` (verified: it returned /etc/hosts), so an attacker on the tailnet could read arbitrary files (.env, ~/.ssh, …). Keep strict on and set an explicit `server.fs.allow`: the worktree plus the nearest ancestor whose node_modules holds real packages (the main checkout, since a worktree's own node_modules is cache-only and deps resolve up). Vite's default deny list then blocks everything outside that subtree. Verified: /etc/hosts and ~/.zshrc now 403; the app and its main-checkout deps (e.g. vite client) still serve 200. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 991bbe9 commit 20ea57c

1 file changed

Lines changed: 28 additions & 4 deletions

File tree

scripts/vite-qa.config.mjs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import { defineConfig, mergeConfig } from 'vite'
2+
import { fileURLToPath } from 'node:url'
3+
import { existsSync } from 'node:fs'
4+
import path from 'node:path'
25
import base from '../vite.config.ts'
36

47
// QA-only vite config for running a dev server INSIDE a git worktree.
58
//
69
// A worktree has no node_modules of its own — deps resolve up to the main
710
// checkout — so vite's default `server.fs.allow` (rooted at the worktree)
8-
// rejects every dependency request with 403 and the app hangs on "Loading…".
9-
// Relaxing `fs.strict` lets the harness server serve those parent deps. Dev/QA
10-
// only; never use for a build or a server exposed beyond localhost.
11+
// rejects every parent-dependency request with 403 and the app hangs on
12+
// "Loading…".
13+
//
14+
// Do NOT fix this by disabling `fs.strict`: that makes Vite serve ANY local
15+
// file over `/@fs/...` (e.g. /etc/hosts, ~/.ssh, .env), which is a real hazard
16+
// because this server is reached over the Tailscale tunnel during iPad testing
17+
// (VITE_TUNNEL in vite.config.ts), not just localhost. Instead keep strict on
18+
// and explicitly allow the worktree plus the checkout that actually holds
19+
// node_modules — Vite's default deny list still blocks everything outside.
1120
//
1221
// Kept as plain .mjs (like webkit-qa.mjs) so `tsc -b` doesn't typecheck it
1322
// against the app's vite config types.
@@ -16,7 +25,22 @@ import base from '../vite.config.ts'
1625
// --config scripts/vite-qa.config.mjs --port 5199 --strictPort
1726
//
1827
// (run from the worktree root; node resolves vite up to the main checkout.)
28+
const worktreeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
29+
// Walk up to the nearest ancestor whose node_modules holds REAL packages (the
30+
// main checkout, when run from a worktree; the worktree itself for a normal
31+
// checkout). A worktree's own node_modules is cache-only (.vite/.cache) — node
32+
// resolves actual deps up the tree — so probe for `node_modules/vite` (the dev
33+
// server we're running) rather than a bare node_modules dir. depsRoot contains
34+
// the worktree, so allowing it covers both source and the resolved deps.
35+
let depsRoot = worktreeRoot
36+
while (
37+
!existsSync(path.join(depsRoot, 'node_modules', 'vite')) &&
38+
path.dirname(depsRoot) !== depsRoot
39+
) {
40+
depsRoot = path.dirname(depsRoot)
41+
}
42+
1943
export default defineConfig(async (env) => {
2044
const resolved = typeof base === 'function' ? await base(env) : base
21-
return mergeConfig(resolved, { server: { fs: { strict: false } } })
45+
return mergeConfig(resolved, { server: { fs: { allow: [worktreeRoot, depsRoot] } } })
2246
})

0 commit comments

Comments
 (0)