// Which local files a command may hand to the browser's file inputs. // // The broker runs on the browser's host, so a path in an upload command names a file on that // machine, and nothing else checks it: the companion extension passes it to // DOM.setFileInputFiles as given. Anyone holding the broker's token could attach any file the // user can read to a page. Clients are told to stage uploads in one directory; this enforces it. // // Covers chrome_upload_file (page.upload) and the same CDP method through chrome_cdp (cdp.call). // Paths are resolved with realpath first, so a symlink or ".." cannot lead out of the directory. import { realpathSync } from "node:fs"; import { sep } from "node:path"; export const DEFAULT_UPLOAD_DIR = "/tmp/pi-chrome-uploads"; function resolveReal(path) { try { return realpathSync(path); } catch { return undefined; // missing: nothing to upload, and not ours to guess at } } function pathsOf(action, params) { if (action === "page.upload") return Array.isArray(params?.paths) ? params.paths : []; if (action === "cdp.call" && params?.method === "DOM.setFileInputFiles") { const files = params.params?.files; return Array.isArray(files) ? files : []; } return []; } // Throws if a command would upload a file from outside uploadDir. export function assertUploadsStaged(action, params, uploadDir = DEFAULT_UPLOAD_DIR) { const paths = pathsOf(action, params); if (paths.length === 0) return; const root = resolveReal(uploadDir); for (const path of paths) { const real = typeof path === "string" && path.startsWith("/") ? resolveReal(path) : undefined; if (root === undefined || real === undefined || !real.startsWith(root + sep)) { throw new Error(`Uploads are limited to files in ${uploadDir} on the browser's host; "${path}" is not one. Copy the file there first.`); } } }