garrytan/gstack · error · Error
load-html: --from-file ${payloadPath} must be under ${SAFE_D
Error message
load-html: --from-file ${payloadPath} must be under ${SAFE_DIRECTORIES.join(' or ')} (security policy). Copy the payload into the project tree or /tmp first. What it means
Security policy at write-commands.ts:200-203. The --from-file payload path is run through validateReadPath (path-security.ts:78), which resolves the path, follows symlinks via realpathSync, and confirms it sits under SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()]. If not, the read is refused so that an attacker-influenced path cannot read arbitrary files. The comment explicitly flags this as parity with the sibling file-path branch.
Source
Thrown at browse/src/write-commands.ts:202
// The safe-dirs + magic-byte + size-cap checks below still apply to the
// INLINE HTML content, not to the payload file path itself.
let fromFilePayload: { html: string; waitUntil?: SetContentWaitUntil } | null = null;
let filePath: string | undefined;
let waitUntil: SetContentWaitUntil = 'domcontentloaded';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--from-file') {
const payloadPath = args[++i];
if (!payloadPath) throw new Error('load-html: --from-file requires a path');
// Parity with the sibling `load-html <file>` path below (line 249):
// that branch runs every `file://` target through validateReadPath
// so the safe-dirs policy can't be side-stepped. Same policy must
// apply here — otherwise --from-file becomes a read-anywhere escape
// hatch for any caller that can pick the payload path (e.g., an
// MCP caller issuing load-html with an attacker-influenced path).
try {
validateReadPath(path.resolve(payloadPath));
} catch {
throw new Error(
`load-html: --from-file ${payloadPath} must be under ${SAFE_DIRECTORIES.join(' or ')} (security policy). Copy the payload into the project tree or /tmp first.`
);
}
const raw = fs.readFileSync(payloadPath, 'utf8');
let json: any;
try { json = JSON.parse(raw); }
catch (e: any) { throw new Error(`load-html: --from-file JSON parse failed: ${e.message}`); }
if (typeof json.html !== 'string') {
throw new Error('load-html: --from-file JSON must have a "html" string field');
}
if (json.waitUntil && json.waitUntil !== 'load'
&& json.waitUntil !== 'domcontentloaded' && json.waitUntil !== 'networkidle') {
throw new Error(`load-html: --from-file waitUntil '${json.waitUntil}' invalid`);
}
fromFilePayload = { html: json.html, waitUntil: json.waitUntil };
} else if (args[i] === '--wait-until') {
const val = args[++i];
if (val !== 'load' && val !== 'domcontentloaded' && val !== 'networkidle') {View on GitHub (pinned to 94993f7401)
Solutions
- Copy the payload JSON into the project tree (cwd) or /tmp first
- Start the browse server from the directory containing the payload so cwd matches
- Remove or fix symlinks that escape the safe directories
- Confirm the realpath of the file: readlink -f <path> is under cwd or TEMP_DIR
Example fix
// before
await handleWriteCommand('load-html', ['--from-file','/home/me/secret/p.json'], session, bm)
// after — copy into the project tree or /tmp
cp /home/me/secret/p.json ./p.json
await handleWriteCommand('load-html', ['--from-file','./p.json'], session, bm) Defensive patterns
Strategy: validation
Validate before calling
import * as path from 'node:path'
import { SAFE_DIRECTORIES } from './path-security'
import { isPathWithin } from './platform'
function isUnderSafeDir(p: string): boolean {
return SAFE_DIRECTORIES.some(d => isPathWithin(path.resolve(p), d))
} Type guard
function isSafeReadPath(p: string): boolean {
return SAFE_DIRECTORIES.some(d => isPathWithin(path.resolve(p), d))
} Prevention
- Keep payloads inside the project tree (cwd) or /tmp
- Resolve and realpath-check paths before passing them in
- Watch for symlinks that escape safe directories
When it happens
Trigger: Pointing --from-file at anything outside cwd or TEMP_DIR: /etc/passwd, ~/.ssh/id_rsa, /root/secret.json, or a relative path that realpath-resolves outside the safe dirs (including via a symlink).
Common situations: Absolute path to a home/system file; a relative ../ escape; a symlink inside the project that points outside; running the browse server from a cwd that does not contain the payload.
Related errors
- pdf: --from-file ${payloadPath} must be under ${SAFE_DIRECTO
- Cannot use load-html inside a frame. Run 'frame main' first.
- load-html: --from-file requires a path
- load-html: --from-file JSON parse failed: ${e.message}
- load-html: --from-file JSON must have a "html" string field
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/cb759d307f657322.
Report an issue: GitHub.