{"record":{"id":"cb759d307f657322","repo":"garrytan/gstack","slug":"load-html-from-file-payloadpath-must-be-unde","errorCode":null,"errorMessage":"load-html: --from-file ${payloadPath} must be under ${SAFE_DIRECTORIES.join(' or ')} (security policy). Copy the payload into the project tree or /tmp first.","messagePattern":"load-html: --from-file (.+?) must be under (.+?) \\(security policy\\)\\. Copy the payload into the project tree or /tmp first\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"browse/src/write-commands.ts","lineNumber":202,"sourceCode":"      // The safe-dirs + magic-byte + size-cap checks below still apply to the\n      // INLINE HTML content, not to the payload file path itself.\n      let fromFilePayload: { html: string; waitUntil?: SetContentWaitUntil } | null = null;\n      let filePath: string | undefined;\n      let waitUntil: SetContentWaitUntil = 'domcontentloaded';\n      for (let i = 0; i < args.length; i++) {\n        if (args[i] === '--from-file') {\n          const payloadPath = args[++i];\n          if (!payloadPath) throw new Error('load-html: --from-file requires a path');\n          // Parity with the sibling `load-html <file>` path below (line 249):\n          // that branch runs every `file://` target through validateReadPath\n          // so the safe-dirs policy can't be side-stepped. Same policy must\n          // apply here — otherwise --from-file becomes a read-anywhere escape\n          // hatch for any caller that can pick the payload path (e.g., an\n          // MCP caller issuing load-html with an attacker-influenced path).\n          try {\n            validateReadPath(path.resolve(payloadPath));\n          } catch {\n            throw new Error(\n              `load-html: --from-file ${payloadPath} must be under ${SAFE_DIRECTORIES.join(' or ')} (security policy). Copy the payload into the project tree or /tmp first.`\n            );\n          }\n          const raw = fs.readFileSync(payloadPath, 'utf8');\n          let json: any;\n          try { json = JSON.parse(raw); }\n          catch (e: any) { throw new Error(`load-html: --from-file JSON parse failed: ${e.message}`); }\n          if (typeof json.html !== 'string') {\n            throw new Error('load-html: --from-file JSON must have a \"html\" string field');\n          }\n          if (json.waitUntil && json.waitUntil !== 'load'\n              && json.waitUntil !== 'domcontentloaded' && json.waitUntil !== 'networkidle') {\n            throw new Error(`load-html: --from-file waitUntil '${json.waitUntil}' invalid`);\n          }\n          fromFilePayload = { html: json.html, waitUntil: json.waitUntil };\n        } else if (args[i] === '--wait-until') {\n          const val = args[++i];\n          if (val !== 'load' && val !== 'domcontentloaded' && val !== 'networkidle') {","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/browse/src/write-commands.ts#L184-L220","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nawait handleWriteCommand('load-html', ['--from-file','/home/me/secret/p.json'], session, bm)\n// after — copy into the project tree or /tmp\ncp /home/me/secret/p.json ./p.json\nawait handleWriteCommand('load-html', ['--from-file','./p.json'], session, bm)","handlingStrategy":"validation","validationCode":"import * as path from 'node:path'\nimport { SAFE_DIRECTORIES } from './path-security'\nimport { isPathWithin } from './platform'\nfunction isUnderSafeDir(p: string): boolean {\n  return SAFE_DIRECTORIES.some(d => isPathWithin(path.resolve(p), d))\n}","typeGuard":"function isSafeReadPath(p: string): boolean {\n  return SAFE_DIRECTORIES.some(d => isPathWithin(path.resolve(p), d))\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["path-security","safe-dirs","load-html","security","write-commands"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}