blob: dde8cdf330246e419abed7c6d439af8132fa515d (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// 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.`);
}
}
}
|