@cedarjs/api 8.0.0-canary.3450 → 8.0.0-canary.3451

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,18 +39,6 @@ Note: CedarJS logging is setup for its api side only. For browser and web side e
39
39
 
40
40
  For detailed logger configuration, see the CedarJS logger package [README](./src/logger/README.md).
41
41
 
42
- ### Sandbox
43
-
44
- `@cedarjs/api/sandbox` runs a Node script on untrusted input — an uploaded
45
- photo, a third party's PDF — in a locked-down child process: none of your app's environment
46
- variables (on Windows a few system ones remain), read access only to its
47
- script, the packages it imports and the paths you allow, no writes, no
48
- child processes, no network, a heap cap, a timeout and output limits. Use it
49
- for parsing, decoding or converting files you didn't create (sharp, pdf.js,
50
- …). It's defense in depth on top of Node's permission model, not a security
51
- boundary, and native addons aren't limited by that model. See the sandbox
52
- [README](./src/sandbox/README.md).
53
-
54
42
  ## Contributing
55
43
 
56
44
  `@cedarjs/api` uses a few things you should be familiar with:
package/dist/index.js CHANGED
@@ -8,9 +8,9 @@ export * from "./cors.js";
8
8
  export * from "./event.js";
9
9
  export * from "./signedTokens/index.js";
10
10
  const prismaVersion = "7.10.0";
11
- const redwoodVersion = "8.0.0-canary.3450";
12
- const cedarjsVersion = "8.0.0-canary.3450";
13
- const cedarVersion = "8.0.0-canary.3450";
11
+ const redwoodVersion = "8.0.0-canary.3451";
12
+ const cedarjsVersion = "8.0.0-canary.3451";
13
+ const cedarVersion = "8.0.0-canary.3451";
14
14
  export {
15
15
  cedarVersion,
16
16
  cedarjsVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/api",
3
- "version": "8.0.0-canary.3450",
3
+ "version": "8.0.0-canary.3451",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/cedarjs/cedar.git",
@@ -40,12 +40,6 @@
40
40
  "default": "./dist/logger/index.js"
41
41
  }
42
42
  },
43
- "./sandbox": {
44
- "default": {
45
- "types": "./dist/sandbox/index.d.ts",
46
- "default": "./dist/sandbox/index.js"
47
- }
48
- },
49
43
  "./webhooks": {
50
44
  "default": {
51
45
  "types": "./dist/webhooks/index.d.ts",
@@ -90,7 +84,7 @@
90
84
  "title-case": "3.0.3"
91
85
  },
92
86
  "devDependencies": {
93
- "@cedarjs/framework-tools": "8.0.0-canary.3450",
87
+ "@cedarjs/framework-tools": "8.0.0-canary.3451",
94
88
  "@types/aws-lambda": "8.10.164",
95
89
  "@types/jsonwebtoken": "9.0.10",
96
90
  "@types/memjs": "1",
@@ -100,7 +94,6 @@
100
94
  "memjs": "1.3.2",
101
95
  "publint": "0.3.25",
102
96
  "redis": "6.3.0",
103
- "sharp": "0.35.5",
104
97
  "split2": "4.2.0",
105
98
  "ts-toolbelt": "9.6.0",
106
99
  "typescript": "5.9.3",
@@ -1,22 +0,0 @@
1
- /**
2
- * Runs `fn` once a slot is free; resolves or rejects with what `fn` does.
3
- */
4
- export type ConcurrencyLimiter = <T>(fn: () => Promise<T>) => Promise<T>;
5
- /**
6
- * A limiter that lets at most `max` tasks run at once; the rest wait their
7
- * turn, first come first served. Use it around `runSandboxedChild()`: each
8
- * sandboxed child is a whole Node process, and a burst of uploads shouldn't
9
- * start a hundred of them.
10
- *
11
- * The limit is per limiter, in this process. Create one per kind of work
12
- * at module level, not one per call.
13
- *
14
- * @example
15
- * ```ts
16
- * const limit = createConcurrencyLimiter(2)
17
- *
18
- * const result = await limit(() => runSandboxedChild({ ... }))
19
- * ```
20
- */
21
- export declare const createConcurrencyLimiter: (max: number) => ConcurrencyLimiter;
22
- //# sourceMappingURL=concurrencyLimiter.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"concurrencyLimiter.d.ts","sourceRoot":"","sources":["../../src/sandbox/concurrencyLimiter.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;AAExE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,KAAG,kBAkCtD,CAAA"}
@@ -1,33 +0,0 @@
1
- const createConcurrencyLimiter = (max) => {
2
- if (!Number.isInteger(max) || max < 1) {
3
- throw new Error(`max must be a positive integer, got ${max}`);
4
- }
5
- let running = 0;
6
- const waiting = [];
7
- const acquire = async () => {
8
- if (running < max) {
9
- running += 1;
10
- return;
11
- }
12
- await new Promise((resolve) => waiting.push(resolve));
13
- };
14
- const release = () => {
15
- const next = waiting.shift();
16
- if (next) {
17
- next();
18
- } else {
19
- running -= 1;
20
- }
21
- };
22
- return async (fn) => {
23
- await acquire();
24
- try {
25
- return await fn();
26
- } finally {
27
- release();
28
- }
29
- };
30
- };
31
- export {
32
- createConcurrencyLimiter
33
- };
@@ -1,29 +0,0 @@
1
- /**
2
- * Finds a sandboxed child's script for the module that runs it. Call it
3
- * with `import.meta.dirname` and the script's name without extension, the
4
- * same way in every build:
5
- *
6
- * - from source (tests, `cedar exec`), `<baseName>.ts` (or `.js`) in
7
- * `dir`, which Node runs by stripping its types;
8
- * - in a regular build, `<baseName>.js` in `dir`, where the build puts it
9
- * next to its caller;
10
- * - in a bundled build (`cedar build --ud`, and so on Netlify and Vercel),
11
- * where the caller is part of a bundle,
12
- * `api/dist/ud/sandbox-children/<baseName>.js` in the app (looked for
13
- * from `dir` up). A bundled build needs each child's name to be unique
14
- * in `api/src`.
15
- *
16
- * Throws when there's no such script, so a build that lost it fails loudly
17
- * instead of running without it.
18
- *
19
- * Keep the child script plain enough to run as-is: no path aliases like
20
- * `src/…`, no TypeScript-only syntax such as enums, and imports of packages
21
- * only, never of your app's own modules.
22
- *
23
- * @example
24
- * ```ts
25
- * const script = findChildScript(import.meta.dirname, 'resize.child')
26
- * ```
27
- */
28
- export declare const findChildScript: (dir: string, baseName: string) => string;
29
- //# sourceMappingURL=findChildScript.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"findChildScript.d.ts","sourceRoot":"","sources":["../../src/sandbox/findChildScript.ts"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,EAAE,UAAU,MAAM,KAAG,MAiB/D,CAAA"}
@@ -1,35 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- const SANDBOX_CHILDREN_DIR = path.join("api", "dist", "ud", "sandbox-children");
4
- const findBundledChild = (dir, baseName) => {
5
- let current = dir;
6
- for (; ; ) {
7
- const candidate = path.join(current, SANDBOX_CHILDREN_DIR, `${baseName}.js`);
8
- if (fs.existsSync(candidate)) {
9
- return candidate;
10
- }
11
- const parent = path.dirname(current);
12
- if (parent === current) {
13
- return null;
14
- }
15
- current = parent;
16
- }
17
- };
18
- const findChildScript = (dir, baseName) => {
19
- for (const name of [`${baseName}.js`, `${baseName}.ts`]) {
20
- const candidate = path.join(dir, name);
21
- if (fs.existsSync(candidate)) {
22
- return candidate;
23
- }
24
- }
25
- const bundled = findBundledChild(dir, baseName);
26
- if (bundled) {
27
- return bundled;
28
- }
29
- throw new Error(
30
- `findChildScript: no ${baseName}.js or ${baseName}.ts in ${dir}, and no ${path.join(SANDBOX_CHILDREN_DIR, `${baseName}.js`)} from it up`
31
- );
32
- };
33
- export {
34
- findChildScript
35
- };
@@ -1,11 +0,0 @@
1
- /**
2
- * Run code on untrusted input in a sandboxed, locked-down child process.
3
- * See `runSandboxedChild()` and ./README.md.
4
- */
5
- export { DEFAULT_SANDBOX_HEAP_MB, DEFAULT_SANDBOX_MAX_INPUT_BYTES, DEFAULT_SANDBOX_MAX_OUTPUT_BYTES, DEFAULT_SANDBOX_TIMEOUT_MS, runSandboxedChild, sandboxedChildArgs, } from './runSandboxedChild.js';
6
- export type { RunSandboxedChildOptions, SandboxedChildArgsOptions, SandboxedChildFailure, SandboxedChildResult, } from './runSandboxedChild.js';
7
- export { createConcurrencyLimiter } from './concurrencyLimiter.js';
8
- export type { ConcurrencyLimiter } from './concurrencyLimiter.js';
9
- export { permissionModelBlocksNetwork } from './noNetworkPreload.js';
10
- export { findChildScript } from './findChildScript.js';
11
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sandbox/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,gCAAgC,EAChC,0BAA0B,EAC1B,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EACV,wBAAwB,EACxB,yBAAyB,EACzB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAA;AAClE,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA"}
@@ -1,22 +0,0 @@
1
- import {
2
- DEFAULT_SANDBOX_HEAP_MB,
3
- DEFAULT_SANDBOX_MAX_INPUT_BYTES,
4
- DEFAULT_SANDBOX_MAX_OUTPUT_BYTES,
5
- DEFAULT_SANDBOX_TIMEOUT_MS,
6
- runSandboxedChild,
7
- sandboxedChildArgs
8
- } from "./runSandboxedChild.js";
9
- import { createConcurrencyLimiter } from "./concurrencyLimiter.js";
10
- import { permissionModelBlocksNetwork } from "./noNetworkPreload.js";
11
- import { findChildScript } from "./findChildScript.js";
12
- export {
13
- DEFAULT_SANDBOX_HEAP_MB,
14
- DEFAULT_SANDBOX_MAX_INPUT_BYTES,
15
- DEFAULT_SANDBOX_MAX_OUTPUT_BYTES,
16
- DEFAULT_SANDBOX_TIMEOUT_MS,
17
- createConcurrencyLimiter,
18
- findChildScript,
19
- permissionModelBlocksNetwork,
20
- runSandboxedChild,
21
- sandboxedChildArgs
22
- };
@@ -1,16 +0,0 @@
1
- /**
2
- * Whether this Node's permission model restricts network access. On Node
3
- * 25 and later it does: under `--permission`, without `--allow-net`, a
4
- * child can't open sockets, listen, look up names or `fetch`. On Node 24
5
- * it doesn't, so `runSandboxedChild()` loads a preload that takes the
6
- * network away from JavaScript instead (see `NO_NETWORK_PRELOAD`).
7
- *
8
- * Neither limits native code: an addon makes its own system calls.
9
- */
10
- export declare const permissionModelBlocksNetwork: () => boolean;
11
- /**
12
- * The `--import` argument that loads the no-network preload, as a `data:`
13
- * URL. Loading it needs no file and no read permission.
14
- */
15
- export declare const noNetworkPreloadArg: () => string;
16
- //# sourceMappingURL=noNetworkPreload.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"noNetworkPreload.d.ts","sourceRoot":"","sources":["../../src/sandbox/noNetworkPreload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,eACe,CAAA;AAiIxD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,cAC2C,CAAA"}
@@ -1,109 +0,0 @@
1
- const permissionModelBlocksNetwork = () => process.allowedNodeEnvironmentFlags.has("--allow-net");
2
- const NO_NETWORK_PRELOAD = String.raw`
3
- const BLOCKED = new Set([
4
- 'net',
5
- 'tls',
6
- 'dgram',
7
- 'dns',
8
- 'dns/promises',
9
- 'http',
10
- 'https',
11
- 'http2',
12
- '_http_agent',
13
- '_http_client',
14
- '_http_common',
15
- '_http_incoming',
16
- '_http_outgoing',
17
- '_http_server',
18
- '_tls_common',
19
- '_tls_wrap',
20
- ])
21
-
22
- const isBlocked = (id) =>
23
- typeof id === 'string' && BLOCKED.has(id.replace(/^node:/, ''))
24
-
25
- const refuse = (what) =>
26
- function noNetwork() {
27
- throw new Error('No network access in a sandboxed child (' + what + ')')
28
- }
29
-
30
- const getBuiltinModule = process.getBuiltinModule.bind(process)
31
- const { registerHooks, syncBuiltinESMExports } =
32
- getBuiltinModule('node:module')
33
-
34
- // The modules themselves first, before the hooks below make them
35
- // unreachable. Builtin modules are shared, so Node's own users of them
36
- // (http uses net) get the patched functions too
37
- const net = getBuiltinModule('node:net')
38
- net.Socket.prototype.connect = refuse('net.Socket#connect')
39
- net.Server.prototype.listen = refuse('net.Server#listen')
40
- net.connect = refuse('net.connect')
41
- net.createConnection = refuse('net.createConnection')
42
-
43
- const tls = getBuiltinModule('node:tls')
44
- tls.connect = refuse('tls.connect')
45
-
46
- const dgram = getBuiltinModule('node:dgram')
47
- for (const name of ['bind', 'connect', 'send']) {
48
- dgram.Socket.prototype[name] = refuse('dgram.Socket#' + name)
49
- }
50
-
51
- const dns = getBuiltinModule('node:dns')
52
- for (const [label, api] of [
53
- ['dns', dns],
54
- ['dns.promises', dns.promises],
55
- ]) {
56
- for (const name of Object.keys(api)) {
57
- if (typeof api[name] === 'function') {
58
- api[name] = refuse(label + '.' + name)
59
- }
60
- }
61
- }
62
-
63
- // The handles under stdin, stdout and stderr (a Pipe or a TTY). Their
64
- // class can make new handles that connect or bind, e.g. to a Unix socket.
65
- // Reading process.stdin and friends here also opens the child's own stdio
66
- // before open() is taken away
67
- for (const stream of [process.stdin, process.stdout, process.stderr]) {
68
- const handle = stream._handle
69
- const proto = handle && Object.getPrototypeOf(handle)
70
- if (!proto) {
71
- continue
72
- }
73
- for (const name of ['bind', 'bind6', 'connect', 'connect6', 'listen', 'open']) {
74
- if (typeof proto[name] === 'function') {
75
- proto[name] = refuse(handle.constructor.name + '#' + name)
76
- }
77
- }
78
- }
79
-
80
- syncBuiltinESMExports()
81
-
82
- // Then the ways to the modules
83
- registerHooks({
84
- resolve(specifier, context, nextResolve) {
85
- if (isBlocked(specifier)) {
86
- refuse('import of ' + specifier)()
87
- }
88
- return nextResolve(specifier, context)
89
- },
90
- })
91
-
92
- process.getBuiltinModule = (id) => {
93
- if (isBlocked(id)) {
94
- refuse('process.getBuiltinModule(' + id + ')')()
95
- }
96
- return getBuiltinModule(id)
97
- }
98
-
99
- for (const name of ['fetch', 'WebSocket', 'EventSource']) {
100
- delete globalThis[name]
101
- }
102
-
103
- //# sourceURL=cedar-sandbox-no-network-preload.js
104
- `;
105
- const noNetworkPreloadArg = () => `--import=data:text/javascript,${encodeURIComponent(NO_NETWORK_PRELOAD)}`;
106
- export {
107
- noNetworkPreloadArg,
108
- permissionModelBlocksNetwork
109
- };
@@ -1,186 +0,0 @@
1
- /** The heap cap for a sandboxed child, in MB, unless `heapMb` is given */
2
- export declare const DEFAULT_SANDBOX_HEAP_MB = 256;
3
- /** How long a sandboxed child may run, unless `timeoutMs` is given */
4
- export declare const DEFAULT_SANDBOX_TIMEOUT_MS = 10000;
5
- /** The most bytes sent to a sandboxed child, unless `maxInputBytes` is given */
6
- export declare const DEFAULT_SANDBOX_MAX_INPUT_BYTES: number;
7
- /** The most bytes read back from a sandboxed child, unless `maxOutputBytes` is given */
8
- export declare const DEFAULT_SANDBOX_MAX_OUTPUT_BYTES: number;
9
- export interface SandboxedChildArgsOptions {
10
- /**
11
- * The Node script the child runs: a `.js`/`.mjs`/`.cjs` file, or a `.ts`
12
- * file Node can run by stripping its types. It may read itself.
13
- */
14
- script: string;
15
- /**
16
- * The packages the child imports, by name (`['sharp']`). The child may
17
- * read each one's folder and those of everything it depends on
18
- * (dependencies, and installed optional and peer dependencies, which is
19
- * where native packages keep their platform's binaries), resolved from
20
- * the script's folder the way Node will resolve its imports. Nothing
21
- * else in `node_modules` is readable. A package that isn't installed
22
- * there fails the run (`'spawn-error'`) before a child starts.
23
- */
24
- packages?: string[];
25
- /**
26
- * Files and directories (and everything under them) the child may read
27
- * besides its own script and its `packages`. Everything else on disk is
28
- * unreadable to the child: `.env`, your app's source, uploaded files.
29
- */
30
- readable?: string[];
31
- /**
32
- * Whether the child may load native addons (`.node` files), as a library
33
- * like sharp needs. Off by default. Node's permission model does not
34
- * apply to native code: an addon, or an attacker who takes over one, can
35
- * do anything the OS lets the process do. Turn this on only for fixed,
36
- * known libraries, never for an addon the input could choose.
37
- */
38
- allowAddons?: boolean;
39
- /** The child's heap cap (`--max-old-space-size`), in MB. Default 256. */
40
- heapMb?: number;
41
- /** Arguments for the child script, in `process.argv.slice(2)` */
42
- args?: string[];
43
- }
44
- /**
45
- * The `node` command-line arguments for running `script` as a sandboxed
46
- * child process: Node's permission model (`--permission`) with read access
47
- * to `script`, its `packages` and `readable` only, no network, a heap cap, and code generation from
48
- * strings (`eval`, `new Function`) turned off. No file writes, no child
49
- * processes, no worker threads, no WASI, no inspector, and native addons
50
- * only with `allowAddons`.
51
- *
52
- * On Node 25 and later the permission model blocks the network. On Node 24
53
- * it doesn't, so the arguments also load a preload that takes the network
54
- * away from JavaScript in the child (see `permissionModelBlocksNetwork()`).
55
- *
56
- * `runSandboxedChild()` uses this; it's exported for spawning the child
57
- * yourself (e.g. in a test probing what the child can do). Pass an empty
58
- * `env` when you do: the arguments restrict what the child may touch, not
59
- * what it inherits.
60
- *
61
- * The permission model checks the path a file is opened by, and Node opens
62
- * the script and the modules it imports by their real paths, so the
63
- * script is given by its real path, and other paths by both (an app
64
- * deployed behind a `current -> releases/…` symlink would otherwise be
65
- * refused its own files). Throws if a path doesn't exist.
66
- */
67
- export declare const sandboxedChildArgs: ({ script, packages, readable, allowAddons, heapMb, args, }: SandboxedChildArgsOptions) => string[];
68
- export interface RunSandboxedChildOptions extends SandboxedChildArgsOptions {
69
- /** The untrusted bytes, written to the child's stdin */
70
- input: Uint8Array;
71
- /**
72
- * The child is killed (SIGKILL) if it hasn't exited after this many
73
- * milliseconds. Default 10 000. Like the byte limits, it must be a finite
74
- * number from 0 up; anything else (`NaN` from a missing setting, say) is
75
- * a `'spawn-error'` and no child is started.
76
- */
77
- timeoutMs?: number;
78
- /**
79
- * Larger input is refused without starting the child. Default 25 MiB.
80
- */
81
- maxInputBytes?: number;
82
- /**
83
- * The child is killed as soon as it has written more than this many bytes
84
- * to stdout. Default 25 MiB.
85
- */
86
- maxOutputBytes?: number;
87
- /** The child's working directory. Default: the script's folder. */
88
- cwd?: string;
89
- }
90
- /** Why a sandboxed child produced no output */
91
- export type SandboxedChildFailure =
92
- /** The input was over `maxInputBytes`; no child was started */
93
- 'input-too-large'
94
- /** The child ran past `timeoutMs` and was killed */
95
- | 'timeout'
96
- /** The child wrote more than `maxOutputBytes` and was killed */
97
- | 'output-too-large'
98
- /** The child exited non-zero or was killed by a signal */
99
- | 'exit'
100
- /** The child couldn't be started, or a limit isn't a valid number */
101
- | 'spawn-error';
102
- export type SandboxedChildResult = {
103
- ok: true;
104
- /** Everything the child wrote to stdout */
105
- output: Buffer;
106
- /** What the child wrote to stderr (the last 4000 characters) */
107
- stderr: string;
108
- } | {
109
- ok: false;
110
- failure: SandboxedChildFailure;
111
- /** A one-line description, for a log */
112
- message: string;
113
- /** What the child wrote to stderr (the last 4000 characters) */
114
- stderr: string;
115
- exitCode: number | null;
116
- signal: NodeJS.Signals | null;
117
- };
118
- /**
119
- * Runs a Node script on untrusted bytes in a locked-down (sandboxed) child
120
- * process, so a crafted input that exploits a parser bug lands in a process
121
- * that can't read your secrets or touch your files. Use it for parsing,
122
- * decoding or converting files your users or third parties send you:
123
- * resizing a photo with sharp, rendering a PDF page with pdf.js, unpacking
124
- * an archive, parsing a spreadsheet.
125
- *
126
- * The child:
127
- *
128
- * - gets an **empty environment** — no `DATABASE_URL`, no API keys, none of
129
- * your `.env`;
130
- * - runs under **Node's permission model**: it may read its own script,
131
- * the `packages` it imports and the `readable` paths,
132
- * nothing else; it may not write files, spawn processes, start workers,
133
- * or `eval`; it may load native addons only with `allowAddons`;
134
- * - has **no network**: no sockets, no `fetch`, no DNS (on Node 24 through
135
- * a preload, see `permissionModelBlocksNetwork()`);
136
- * - gets the input on **stdin**, and its **stdout** is the result, used only
137
- * if it exits with code 0;
138
- * - is **bounded**: a heap cap, a hard timeout (SIGKILL), and caps on input
139
- * and output size.
140
- *
141
- * It never rejects: every failure — a non-zero exit, a timeout, too much
142
- * output, a crash — is an `{ ok: false }` result with a `failure` and a
143
- * `message` for your log. Treat the output as untrusted too, and check it
144
- * (e.g. that it's really a PNG) before using it.
145
- *
146
- * Limit how many run at once with `createConcurrencyLimiter()`: each child
147
- * is a whole Node process.
148
- *
149
- * **This is defense in depth, not a security boundary.** Node documents
150
- * that its permission model does not protect against malicious code that
151
- * is already running in the process. None of it constrains native code: a
152
- * native addon (`allowAddons`), or an exploit in one, makes its own system
153
- * calls, so for those children what holds is the empty environment, the
154
- * separate process and its limits. An external binary like ffmpeg needs an
155
- * OS-level sandbox (a container, seccomp, a separate user).
156
- *
157
- * @example
158
- * ```ts
159
- * import {
160
- * createConcurrencyLimiter,
161
- * findChildScript,
162
- * runSandboxedChild,
163
- * } from '@cedarjs/api/sandbox'
164
- *
165
- * const limit = createConcurrencyLimiter(2)
166
- *
167
- * const result = await limit(() =>
168
- * runSandboxedChild({
169
- * script: findChildScript(import.meta.dirname, 'resize.child'),
170
- * packages: ['sharp'],
171
- * allowAddons: true,
172
- * input: uploadedBytes,
173
- * timeoutMs: 10_000,
174
- * maxOutputBytes: 4 * 1024 * 1024,
175
- * }),
176
- * )
177
- *
178
- * if (result.ok) {
179
- * // result.output: Buffer
180
- * } else {
181
- * logger.warn({ failure: result.failure }, result.message)
182
- * }
183
- * ```
184
- */
185
- export declare const runSandboxedChild: ({ input, timeoutMs, maxInputBytes, maxOutputBytes, cwd, ...argsOptions }: RunSandboxedChildOptions) => Promise<SandboxedChildResult>;
186
- //# sourceMappingURL=runSandboxedChild.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runSandboxedChild.d.ts","sourceRoot":"","sources":["../../src/sandbox/runSandboxedChild.ts"],"names":[],"mappings":"AAaA,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,MAAM,CAAA;AAC1C,sEAAsE;AACtE,eAAO,MAAM,0BAA0B,QAAS,CAAA;AAChD,gFAAgF;AAChF,eAAO,MAAM,+BAA+B,QAAW,CAAA;AACvD,wFAAwF;AACxF,eAAO,MAAM,gCAAgC,QAAW,CAAA;AAQxD,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,GAAI,4DAOhC,yBAAyB,KAAG,MAAM,EA6BpC,CAAA;AAED,MAAM,WAAW,wBAAyB,SAAQ,yBAAyB;IACzE,wDAAwD;IACxD,KAAK,EAAE,UAAU,CAAA;IACjB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,+CAA+C;AAC/C,MAAM,MAAM,qBAAqB;AAC/B,+DAA+D;AAC7D,iBAAiB;AACnB,oDAAoD;GAClD,SAAS;AACX,gEAAgE;GAC9D,kBAAkB;AACpB,0DAA0D;GACxD,MAAM;AACR,qEAAqE;GACnE,aAAa,CAAA;AAEjB,MAAM,MAAM,oBAAoB,GAC5B;IACE,EAAE,EAAE,IAAI,CAAA;IACR,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAA;CACf,GACD;IACE,EAAE,EAAE,KAAK,CAAA;IACT,OAAO,EAAE,qBAAqB,CAAA;IAC9B,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;CAC9B,CAAA;AAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,eAAO,MAAM,iBAAiB,GAAI,0EAO/B,wBAAwB,KAAG,OAAO,CAAC,oBAAoB,CA+LzD,CAAA"}
@@ -1,213 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import {
5
- noNetworkPreloadArg,
6
- permissionModelBlocksNetwork
7
- } from "./noNetworkPreload.js";
8
- import { sandboxPackagePaths } from "./sandboxPackages.js";
9
- const MIB = 1024 * 1024;
10
- const DEFAULT_SANDBOX_HEAP_MB = 256;
11
- const DEFAULT_SANDBOX_TIMEOUT_MS = 1e4;
12
- const DEFAULT_SANDBOX_MAX_INPUT_BYTES = 25 * MIB;
13
- const DEFAULT_SANDBOX_MAX_OUTPUT_BYTES = 25 * MIB;
14
- const MAX_TIMEOUT_MS = 2 ** 31 - 1;
15
- const MAX_STDERR_CHARS = 4e3;
16
- const sandboxedChildArgs = ({
17
- script,
18
- packages = [],
19
- readable = [],
20
- allowAddons = false,
21
- heapMb = DEFAULT_SANDBOX_HEAP_MB,
22
- args = []
23
- }) => {
24
- if (!Number.isInteger(heapMb) || heapMb <= 0) {
25
- throw new Error(`heapMb must be a positive integer, got ${heapMb}`);
26
- }
27
- const realScript = fs.realpathSync(script);
28
- const readPaths = [
29
- .../* @__PURE__ */ new Set([
30
- realScript,
31
- ...sandboxPackagePaths(packages, realScript),
32
- // Node may read a file through the path it was given or through
33
- // its real path
34
- ...readable.flatMap((p) => [path.resolve(p), fs.realpathSync(p)])
35
- ])
36
- ];
37
- return [
38
- `--max-old-space-size=${heapMb}`,
39
- "--disallow-code-generation-from-strings",
40
- "--permission",
41
- ...allowAddons ? ["--allow-addons"] : [],
42
- // Node warns that the permission model is experimental, and about
43
- // --allow-addons; the warnings would end up in the failure message
44
- "--no-warnings",
45
- ...readPaths.map((p) => `--allow-fs-read=${p}`),
46
- ...permissionModelBlocksNetwork() ? [] : [noNetworkPreloadArg()],
47
- realScript,
48
- ...args
49
- ];
50
- };
51
- const runSandboxedChild = ({
52
- input,
53
- timeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS,
54
- maxInputBytes = DEFAULT_SANDBOX_MAX_INPUT_BYTES,
55
- maxOutputBytes = DEFAULT_SANDBOX_MAX_OUTPUT_BYTES,
56
- cwd,
57
- ...argsOptions
58
- }) => {
59
- const fail = (failure, message, rest = {}) => ({
60
- ok: false,
61
- failure,
62
- message,
63
- stderr: rest.stderr ?? "",
64
- exitCode: rest.exitCode ?? null,
65
- signal: rest.signal ?? null
66
- });
67
- const invalidLimit = [
68
- ["timeoutMs", timeoutMs, MAX_TIMEOUT_MS],
69
- ["maxInputBytes", maxInputBytes, Number.MAX_SAFE_INTEGER],
70
- ["maxOutputBytes", maxOutputBytes, Number.MAX_SAFE_INTEGER]
71
- ].find(
72
- ([, value, max]) => typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > Number(max)
73
- );
74
- if (invalidLimit) {
75
- const [name, value, max] = invalidLimit;
76
- return Promise.resolve(
77
- fail(
78
- "spawn-error",
79
- `${name} must be a number from 0 to ${max}, got ${String(value)}`
80
- )
81
- );
82
- }
83
- if (input.length > maxInputBytes) {
84
- return Promise.resolve(
85
- fail(
86
- "input-too-large",
87
- `input is ${input.length} bytes, over the ${maxInputBytes} byte limit`
88
- )
89
- );
90
- }
91
- let args;
92
- try {
93
- args = sandboxedChildArgs(argsOptions);
94
- } catch (e) {
95
- const message = e instanceof Error ? e.message : String(e);
96
- return Promise.resolve(fail("spawn-error", message));
97
- }
98
- return new Promise((resolve) => {
99
- let settled = false;
100
- let stderr = "";
101
- const stderrTail = () => stderr.slice(-MAX_STDERR_CHARS);
102
- let child;
103
- const settle = (result) => {
104
- if (settled) {
105
- return;
106
- }
107
- settled = true;
108
- clearTimeout(timer);
109
- resolve(result);
110
- };
111
- let killedFor = null;
112
- const kill = (failure, message) => {
113
- if (killedFor || settled) {
114
- return;
115
- }
116
- killedFor = { failure, message };
117
- clearTimeout(timer);
118
- child?.kill("SIGKILL");
119
- };
120
- const timer = setTimeout(
121
- () => kill("timeout", `timed out after ${timeoutMs} ms`),
122
- timeoutMs
123
- );
124
- try {
125
- child = spawn(process.execPath, args, {
126
- // Nothing of the parent's environment: no database URL, no keys
127
- env: {},
128
- cwd: cwd ?? path.dirname(fs.realpathSync(argsOptions.script)),
129
- stdio: ["pipe", "pipe", "pipe"],
130
- windowsHide: true
131
- });
132
- } catch (e) {
133
- const message = e instanceof Error ? e.message : String(e);
134
- settle(fail("spawn-error", message));
135
- return;
136
- }
137
- const output = [];
138
- let outputSize = 0;
139
- child.stdout?.on("data", (chunk) => {
140
- if (settled || killedFor) {
141
- return;
142
- }
143
- outputSize += chunk.length;
144
- if (outputSize > maxOutputBytes) {
145
- kill(
146
- "output-too-large",
147
- `output is over the ${maxOutputBytes} byte limit`
148
- );
149
- return;
150
- }
151
- output.push(chunk);
152
- });
153
- child.stderr?.setEncoding("utf8");
154
- child.stderr?.on("data", (chunk) => {
155
- stderr = (stderr + chunk).slice(-MAX_STDERR_CHARS * 2);
156
- });
157
- child.on("error", (e) => {
158
- if (child?.pid === void 0) {
159
- settle(fail("spawn-error", e.message, { stderr: stderrTail() }));
160
- }
161
- });
162
- child.on("exit", (exitCode, signal) => {
163
- if (killedFor) {
164
- settle(
165
- fail(killedFor.failure, killedFor.message, {
166
- stderr: stderrTail(),
167
- exitCode,
168
- signal
169
- })
170
- );
171
- }
172
- });
173
- child.on("close", (exitCode, signal) => {
174
- if (killedFor) {
175
- settle(
176
- fail(killedFor.failure, killedFor.message, {
177
- stderr: stderrTail(),
178
- exitCode,
179
- signal
180
- })
181
- );
182
- return;
183
- }
184
- if (exitCode === 0) {
185
- settle({
186
- ok: true,
187
- output: Buffer.concat(output),
188
- stderr: stderrTail()
189
- });
190
- return;
191
- }
192
- const lastLine = stderrTail().trim().split("\n").filter((line) => line.trim()).pop() ?? "";
193
- settle(
194
- fail(
195
- "exit",
196
- `exited with ${exitCode ?? signal}${lastLine ? `: ${lastLine}` : ""}`,
197
- { stderr: stderrTail(), exitCode, signal }
198
- )
199
- );
200
- });
201
- child.stdin?.on("error", () => {
202
- });
203
- child.stdin?.end(input);
204
- });
205
- };
206
- export {
207
- DEFAULT_SANDBOX_HEAP_MB,
208
- DEFAULT_SANDBOX_MAX_INPUT_BYTES,
209
- DEFAULT_SANDBOX_MAX_OUTPUT_BYTES,
210
- DEFAULT_SANDBOX_TIMEOUT_MS,
211
- runSandboxedChild,
212
- sandboxedChildArgs
213
- };
@@ -1,16 +0,0 @@
1
- /**
2
- * The folders a sandboxed child needs to read to import `packages` from
3
- * `fromFile` (its script): each package's folder and, recursively, those
4
- * of its `dependencies` and installed `optionalDependencies` and
5
- * `peerDependencies`, each resolved from the folder of the package that
6
- * needs it, as Node will at run time. That covers nested versions, pnpm's
7
- * layout and native packages' platform binaries (`@img/sharp-<platform>`)
8
- * without naming a platform. Optional and peer dependencies that aren't
9
- * installed are skipped; a missing package or regular dependency throws.
10
- *
11
- * A package reached through a symlink is granted at both paths: Node reads
12
- * its `package.json` through the path it looked in, and its modules
13
- * through their real paths.
14
- */
15
- export declare const sandboxPackagePaths: (packages: string[], fromFile: string) => string[];
16
- //# sourceMappingURL=sandboxPackages.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"sandboxPackages.d.ts","sourceRoot":"","sources":["../../src/sandbox/sandboxPackages.ts"],"names":[],"mappings":"AAoDA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,EAAE,EAAE,UAAU,MAAM,aAuCvE,CAAA"}
@@ -1,63 +0,0 @@
1
- import fs from "node:fs";
2
- import { createRequire } from "node:module";
3
- import path from "node:path";
4
- const findPackage = (name, fromFile) => {
5
- const passed = [];
6
- for (const dir of createRequire(fromFile).resolve.paths(name) ?? []) {
7
- const found = path.join(dir, name);
8
- if (!fs.existsSync(path.join(found, "package.json"))) {
9
- if (path.basename(dir) === "node_modules" && fs.existsSync(dir)) {
10
- passed.push(
11
- path.join(dir, name, "package.json"),
12
- path.join(fs.realpathSync(dir), name, "package.json")
13
- );
14
- }
15
- continue;
16
- }
17
- const realDir = fs.realpathSync(found);
18
- const pkg = JSON.parse(
19
- fs.readFileSync(path.join(realDir, "package.json"), "utf8")
20
- );
21
- return { found, realDir, pkg, passed };
22
- }
23
- return null;
24
- };
25
- const sandboxPackagePaths = (packages, fromFile) => {
26
- const paths = /* @__PURE__ */ new Set();
27
- const visited = /* @__PURE__ */ new Set();
28
- const visit = (name, from, required, by) => {
29
- const found = findPackage(name, from);
30
- if (!found) {
31
- if (required) {
32
- throw new Error(
33
- `Can't find package "${name}" (needed by ${by}) in any node_modules folder Node looks in from ${path.dirname(from)}`
34
- );
35
- }
36
- return;
37
- }
38
- paths.add(found.found).add(found.realDir);
39
- for (const manifest2 of found.passed) {
40
- paths.add(manifest2);
41
- }
42
- if (visited.has(found.realDir)) {
43
- return;
44
- }
45
- visited.add(found.realDir);
46
- const manifest = path.join(found.realDir, "package.json");
47
- const { dependencies = {}, optionalDependencies = {} } = found.pkg;
48
- const optional = { ...optionalDependencies, ...found.pkg.peerDependencies };
49
- for (const dep of Object.keys(dependencies)) {
50
- visit(dep, manifest, !(dep in optional), name);
51
- }
52
- for (const dep of Object.keys(optional)) {
53
- visit(dep, manifest, false, name);
54
- }
55
- };
56
- for (const name of packages) {
57
- visit(name, fromFile, true, path.basename(fromFile));
58
- }
59
- return [...paths];
60
- };
61
- export {
62
- sandboxPackagePaths
63
- };