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

  1. Copy the payload JSON into the project tree (cwd) or /tmp first
  2. Start the browse server from the directory containing the payload so cwd matches
  3. Remove or fix symlinks that escape the safe directories
  4. 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

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


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/cb759d307f657322. Report an issue: GitHub.