veryfront 0.1.1213 → 0.1.1215
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/esm/deno.js +1 -1
- package/esm/src/agent/conversation/delegation-policy.d.ts +1 -3
- package/esm/src/agent/conversation/delegation-policy.d.ts.map +1 -1
- package/esm/src/agent/conversation/delegation-policy.js +1 -3
- package/esm/src/agent/index.d.ts +1 -1
- package/esm/src/agent/index.d.ts.map +1 -1
- package/esm/src/agent/index.js +1 -1
- package/esm/src/agent/runtime/agent-loop-skill-state.d.ts +10 -12
- package/esm/src/agent/runtime/agent-loop-skill-state.d.ts.map +1 -1
- package/esm/src/agent/runtime/agent-loop-skill-state.js +11 -17
- package/esm/src/agent/runtime/agent-runtime-step.d.ts +0 -1
- package/esm/src/agent/runtime/agent-runtime-step.d.ts.map +1 -1
- package/esm/src/agent/runtime/agent-runtime-step.js +2 -2
- package/esm/src/agent/runtime/index.d.ts +1 -1
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +7 -13
- package/esm/src/agent/runtime/load-skill-tool.d.ts +2 -2
- package/esm/src/agent/runtime/load-skill-tool.d.ts.map +1 -1
- package/esm/src/agent/runtime/load-skill-tool.js +12 -68
- package/esm/src/agent/runtime/project-skill-catalog.d.ts.map +1 -1
- package/esm/src/agent/runtime/project-skill-catalog.js +1 -6
- package/esm/src/agent/runtime/skill-metadata.d.ts +4 -18
- package/esm/src/agent/runtime/skill-metadata.d.ts.map +1 -1
- package/esm/src/agent/runtime/skill-metadata.js +17 -78
- package/esm/src/agent/runtime/skill-policy-enforcement.d.ts +1 -5
- package/esm/src/agent/runtime/skill-policy-enforcement.d.ts.map +1 -1
- package/esm/src/agent/runtime/skill-policy-enforcement.js +12 -55
- package/esm/src/agent/runtime/skill-prompt.d.ts +2 -2
- package/esm/src/agent/runtime/skill-prompt.d.ts.map +1 -1
- package/esm/src/agent/runtime/skill-prompt.js +8 -44
- package/esm/src/html/hydration-script-builder/hydration-runtime.generated.js +1 -1
- package/esm/src/platform/adapters/index.d.ts +25 -0
- package/esm/src/platform/adapters/index.d.ts.map +1 -0
- package/esm/src/platform/adapters/index.js +23 -0
- package/esm/src/platform/adapters/security/index.d.ts +8 -0
- package/esm/src/platform/adapters/security/index.d.ts.map +1 -0
- package/esm/src/platform/adapters/security/index.js +7 -0
- package/esm/src/platform/adapters/token/factory.d.ts +10 -0
- package/esm/src/platform/adapters/token/factory.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/factory.js +79 -0
- package/esm/src/platform/adapters/token/index.d.ts +9 -0
- package/esm/src/platform/adapters/token/index.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/index.js +8 -0
- package/esm/src/platform/adapters/token/integration.d.ts +6 -0
- package/esm/src/platform/adapters/token/integration.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/integration.js +61 -0
- package/esm/src/platform/adapters/token/veryfront/adapter.d.ts +22 -0
- package/esm/src/platform/adapters/token/veryfront/adapter.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/adapter.js +88 -0
- package/esm/src/platform/adapters/token/veryfront/api-client.d.ts +15 -0
- package/esm/src/platform/adapters/token/veryfront/api-client.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/api-client.js +212 -0
- package/esm/src/platform/adapters/token/veryfront/index.d.ts +10 -0
- package/esm/src/platform/adapters/token/veryfront/index.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/index.js +9 -0
- package/esm/src/platform/adapters/token/veryfront/memory-adapter.d.ts +20 -0
- package/esm/src/platform/adapters/token/veryfront/memory-adapter.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/memory-adapter.js +52 -0
- package/esm/src/platform/adapters/token/veryfront/types.d.ts +86 -0
- package/esm/src/platform/adapters/token/veryfront/types.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/types.js +81 -0
- package/esm/src/platform/compat/http/pinned-fetch.d.ts +1 -1
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +17 -1
- package/esm/src/security/http/auth.d.ts.map +1 -1
- package/esm/src/security/http/auth.js +7 -0
- package/esm/src/security/http/config.d.ts +5 -0
- package/esm/src/security/http/config.d.ts.map +1 -1
- package/esm/src/security/http/config.js +16 -1
- package/esm/src/security/http/csp-report-endpoint.d.ts +29 -0
- package/esm/src/security/http/csp-report-endpoint.d.ts.map +1 -0
- package/esm/src/security/http/csp-report-endpoint.js +30 -0
- package/esm/src/security/http/csrf/csrf-handler.d.ts.map +1 -1
- package/esm/src/security/http/csrf/csrf-handler.js +6 -0
- package/esm/src/security/http/derived-csp-cache.d.ts +33 -0
- package/esm/src/security/http/derived-csp-cache.d.ts.map +1 -0
- package/esm/src/security/http/derived-csp-cache.js +101 -0
- package/esm/src/security/http/derived-csp-origins.d.ts +54 -0
- package/esm/src/security/http/derived-csp-origins.d.ts.map +1 -0
- package/esm/src/security/http/derived-csp-origins.js +156 -0
- package/esm/src/security/http/response/security-handler.d.ts +1 -2
- package/esm/src/security/http/response/security-handler.d.ts.map +1 -1
- package/esm/src/security/http/response/security-handler.js +25 -9
- package/esm/src/security/sandbox/constants.d.ts +3 -0
- package/esm/src/security/sandbox/constants.d.ts.map +1 -0
- package/esm/src/security/sandbox/constants.js +2 -0
- package/esm/src/security/sandbox/deno-sandbox.d.ts +71 -0
- package/esm/src/security/sandbox/deno-sandbox.d.ts.map +1 -0
- package/esm/src/security/sandbox/deno-sandbox.js +184 -0
- package/esm/src/security/sandbox/permission-system.d.ts +11 -0
- package/esm/src/security/sandbox/permission-system.d.ts.map +1 -0
- package/esm/src/security/sandbox/permission-system.js +85 -0
- package/esm/src/server/handlers/request/csp-report.handler.d.ts +56 -0
- package/esm/src/server/handlers/request/csp-report.handler.d.ts.map +1 -0
- package/esm/src/server/handlers/request/csp-report.handler.js +189 -0
- package/esm/src/server/runtime-handler/index.d.ts +1 -1
- package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/index.js +3 -0
- package/esm/src/server/runtime-handler/project-runtime-context.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/project-runtime-context.js +97 -1
- package/esm/src/server/utils/domain-parser.d.ts +9 -0
- package/esm/src/server/utils/domain-parser.d.ts.map +1 -1
- package/esm/src/server/utils/domain-parser.js +11 -0
- package/esm/src/skill/allowed-tools.d.ts +18 -42
- package/esm/src/skill/allowed-tools.d.ts.map +1 -1
- package/esm/src/skill/allowed-tools.js +31 -182
- package/esm/src/skill/index.d.ts +1 -1
- package/esm/src/skill/index.d.ts.map +1 -1
- package/esm/src/skill/index.js +2 -2
- package/esm/src/skill/parser.d.ts.map +1 -1
- package/esm/src/skill/parser.js +6 -37
- package/esm/src/skill/tools.d.ts.map +1 -1
- package/esm/src/skill/tools.js +2 -3
- package/esm/src/skill/types.d.ts +1 -3
- package/esm/src/skill/types.d.ts.map +1 -1
- package/esm/src/skill/validation.d.ts.map +1 -1
- package/esm/src/skill/validation.js +31 -3
- package/esm/src/types/server.d.ts +10 -0
- package/esm/src/types/server.d.ts.map +1 -1
- package/esm/src/utils/cors-policy-limits.d.ts +9 -2
- package/esm/src/utils/cors-policy-limits.d.ts.map +1 -1
- package/esm/src/utils/cors-policy-limits.js +8 -1
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/esm/src/workflow/api.d.ts +2 -2
- package/package.json +8 -8
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a project's CSP origins from the source it publishes.
|
|
3
|
+
*
|
|
4
|
+
* The platform floor lists what the *platform* emits. Anything a project loads
|
|
5
|
+
* from elsewhere -- a stock-photo host, its own asset CDN, a video origin --
|
|
6
|
+
* has to be admitted separately, and until now the only way to do that was
|
|
7
|
+
* `security.csp`. That made a working site depend on configuration the project
|
|
8
|
+
* had no reason to know it needed: an audit of the hosted fleet found ~100
|
|
9
|
+
* projects loading blocked assets and two that had declared anything.
|
|
10
|
+
*
|
|
11
|
+
* This module closes that gap by reading the origins out of the project's own
|
|
12
|
+
* released source, so `security.csp` becomes an override for what static
|
|
13
|
+
* analysis cannot see rather than a prerequisite for a site that works.
|
|
14
|
+
*
|
|
15
|
+
* Two properties make that safe rather than merely convenient:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Only passive-content directives are derived.** `img-src`, `media-src`
|
|
18
|
+
* and `font-src` decide whether a logo renders. `script-src` and
|
|
19
|
+
* `connect-src` decide whether a third party can execute code in the page
|
|
20
|
+
* or receive data from it. Deriving those would mean anyone able to
|
|
21
|
+
* influence source -- a compromised dependency, a merged pull request,
|
|
22
|
+
* CMS-authored MDX -- could grant themselves execution or an exfiltration
|
|
23
|
+
* endpoint, collapsing two independent barriers into one. Every project in
|
|
24
|
+
* the audit was fixed by passive directives alone.
|
|
25
|
+
*
|
|
26
|
+
* 2. **Origins come from an immutable release.** Derivation runs over the
|
|
27
|
+
* source a release pins, not over live content, so a script injected at
|
|
28
|
+
* runtime cannot extend the allowlist it is subject to.
|
|
29
|
+
*
|
|
30
|
+
* Derivation is deliberately incomplete. A URL assembled at runtime -- a
|
|
31
|
+
* template literal, a CMS field, an environment variable -- is invisible here,
|
|
32
|
+
* and those projects still declare `security.csp`. Under-deriving costs a
|
|
33
|
+
* broken image; over-deriving costs the policy its meaning.
|
|
34
|
+
*
|
|
35
|
+
* @module security/http/derived-csp-origins
|
|
36
|
+
*/
|
|
37
|
+
/** Directives this module is permitted to contribute to. */
|
|
38
|
+
export const DERIVABLE_CSP_DIRECTIVES = Object.freeze(["img-src", "media-src", "font-src"]);
|
|
39
|
+
/**
|
|
40
|
+
* Cap on how much of one file is scanned. A generated bundle or an inlined
|
|
41
|
+
* data blob can be megabytes, and scanning it yields nothing a hand-written
|
|
42
|
+
* source file would not.
|
|
43
|
+
*
|
|
44
|
+
* Measured in UTF-16 code units, matching `String.prototype.length`, because
|
|
45
|
+
* that is the unit the regex engine actually steps through -- 512K code units
|
|
46
|
+
* is the same amount of scanning whether they are ASCII or CJK. A UTF-8 byte
|
|
47
|
+
* cap would read as the more natural bound but would not correspond to the
|
|
48
|
+
* cost being bounded, and computing it means walking the string to decide how
|
|
49
|
+
* much of the string to walk.
|
|
50
|
+
*/
|
|
51
|
+
const MAX_SCANNED_CHARS_PER_FILE = 512 * 1024;
|
|
52
|
+
/**
|
|
53
|
+
* Upper bound on distinct origins, so a generated file cannot inflate the
|
|
54
|
+
* policy. Origins are ranked before the cap applies -- see
|
|
55
|
+
* {@link deriveCspOriginsFromSource}.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_DERIVED_ORIGINS = 32;
|
|
58
|
+
/**
|
|
59
|
+
* Every absolute https URL in the source, whatever syntax surrounds it.
|
|
60
|
+
*
|
|
61
|
+
* An earlier version of this matched by position -- `<img src>`, `url()`,
|
|
62
|
+
* `poster` -- on the theory that knowing which element referenced a URL would
|
|
63
|
+
* let each origin land in exactly the right directive. Measured against real
|
|
64
|
+
* released content that recovers about a third of the references. The same
|
|
65
|
+
* project reaches its asset host through `<img src>`, a JSX `imageSrc` prop,
|
|
66
|
+
* YAML frontmatter, markdown ``, a bare string in a props array, a
|
|
67
|
+
* `srcFallback` key, and `href` on a favicon link. Partial derivation is worse
|
|
68
|
+
* than none: the site still breaks, but now it looks configured.
|
|
69
|
+
*
|
|
70
|
+
* Recall is what protects the project here, and precision is not what protects
|
|
71
|
+
* the visitor -- the passive/active split is. An origin admitted for images
|
|
72
|
+
* that is never referenced costs nothing; one that is missed costs a broken
|
|
73
|
+
* page.
|
|
74
|
+
*/
|
|
75
|
+
const HTTPS_URL_PATTERN = /https:\/\/[a-zA-Z0-9.-]+(?::\d{1,5})?(?![a-zA-Z0-9.:-])/g;
|
|
76
|
+
/**
|
|
77
|
+
* Reduce a URL to the `scheme://host[:port]` form CSP matches on.
|
|
78
|
+
*
|
|
79
|
+
* Returns undefined for anything unparseable, which is the same as declining
|
|
80
|
+
* to derive from it.
|
|
81
|
+
*/
|
|
82
|
+
function toCspOrigin(rawUrl) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = new URL(rawUrl);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== "https:")
|
|
91
|
+
return undefined;
|
|
92
|
+
if (!parsed.hostname)
|
|
93
|
+
return undefined;
|
|
94
|
+
// A hostname with no dot is a bare label, not a public origin; a wildcard is
|
|
95
|
+
// an unfilled template. Neither belongs in a served policy.
|
|
96
|
+
if (!parsed.hostname.includes("."))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (parsed.hostname.includes("*"))
|
|
99
|
+
return undefined;
|
|
100
|
+
return parsed.port ? `https://${parsed.hostname}:${parsed.port}` : `https://${parsed.hostname}`;
|
|
101
|
+
}
|
|
102
|
+
function scanContent(content, into) {
|
|
103
|
+
const scanned = content.length > MAX_SCANNED_CHARS_PER_FILE
|
|
104
|
+
? content.slice(0, MAX_SCANNED_CHARS_PER_FILE)
|
|
105
|
+
: content;
|
|
106
|
+
for (const match of scanned.matchAll(HTTPS_URL_PATTERN)) {
|
|
107
|
+
const origin = toCspOrigin(match[0]);
|
|
108
|
+
if (origin)
|
|
109
|
+
into.set(origin, (into.get(origin) ?? 0) + 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Collect the passive-content origins a project's source references.
|
|
114
|
+
*
|
|
115
|
+
* Pure over its input: callers hand it the file set a release pins, and the
|
|
116
|
+
* result is stable for that set, which is what lets it be computed once per
|
|
117
|
+
* release rather than per request.
|
|
118
|
+
*/
|
|
119
|
+
export function deriveCspOriginsFromSource(files) {
|
|
120
|
+
const counts = new Map();
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
if (!file?.content)
|
|
123
|
+
continue;
|
|
124
|
+
scanContent(file.content, counts);
|
|
125
|
+
}
|
|
126
|
+
if (counts.size === 0)
|
|
127
|
+
return Object.freeze({});
|
|
128
|
+
// Rank by how often an origin is referenced before applying the cap.
|
|
129
|
+
//
|
|
130
|
+
// Truncating in discovery order looked fine until it was run over a real
|
|
131
|
+
// project: one article yielded its asset CDN plus five hosts that appear
|
|
132
|
+
// only as prose links. Across a content site those one-off links vastly
|
|
133
|
+
// outnumber asset hosts, so an arbitrary cut can drop the very origin the
|
|
134
|
+
// site depends on and silently reintroduce the breakage this exists to
|
|
135
|
+
// prevent. An asset host is referenced from every page that uses it; a link
|
|
136
|
+
// in body copy is referenced once. Frequency separates them cleanly.
|
|
137
|
+
//
|
|
138
|
+
// Ties break alphabetically so the result stays deterministic for a given
|
|
139
|
+
// release, which is what lets the header and anything keyed on it be cached.
|
|
140
|
+
const ranked = [...counts.entries()]
|
|
141
|
+
.sort(([originA, countA], [originB, countB]) => countB - countA || originA.localeCompare(originB))
|
|
142
|
+
.slice(0, MAX_DERIVED_ORIGINS)
|
|
143
|
+
.map(([origin]) => origin)
|
|
144
|
+
.sort();
|
|
145
|
+
// Every retained origin is granted to all three passive directives rather
|
|
146
|
+
// than routed between them. Splitting by file extension would reintroduce a
|
|
147
|
+
// failure mode with no compensating benefit: an origin allowed for images
|
|
148
|
+
// but not media is not a boundary anyone reasons about, while guessing wrong
|
|
149
|
+
// leaves the asset blocked. The boundary that matters -- passive content
|
|
150
|
+
// versus script and connect -- is enforced by this list, not by the split.
|
|
151
|
+
const sorted = Object.freeze(ranked);
|
|
152
|
+
const derived = {};
|
|
153
|
+
for (const directive of DERIVABLE_CSP_DIRECTIVES)
|
|
154
|
+
derived[directive] = sorted;
|
|
155
|
+
return Object.freeze(derived);
|
|
156
|
+
}
|
|
@@ -5,8 +5,7 @@ import type { SecurityConfig } from "./types.js";
|
|
|
5
5
|
* security policy. Server integrations must remove project-provided values
|
|
6
6
|
* before applying policy so development omissions remain authoritative.
|
|
7
7
|
*/
|
|
8
|
-
export declare const SECURITY_POLICY_RESPONSE_HEADER_NAMES: readonly ["content-security-policy", "content-security-policy-report-only", "cross-origin-embedder-policy", "cross-origin-opener-policy", "cross-origin-resource-policy", "referrer-policy", "strict-transport-security", "x-content-type-options", "x-frame-options", "x-xss-protection"];
|
|
9
|
-
export declare function isSecurityPolicyResponseHeaderName(name: string): boolean;
|
|
8
|
+
export declare const SECURITY_POLICY_RESPONSE_HEADER_NAMES: readonly ["content-security-policy", "content-security-policy-report-only", "cross-origin-embedder-policy", "cross-origin-opener-policy", "cross-origin-resource-policy", "referrer-policy", "reporting-endpoints", "strict-transport-security", "x-content-type-options", "x-frame-options", "x-xss-protection"];
|
|
10
9
|
export declare function generateNonce(): string;
|
|
11
10
|
/**
|
|
12
11
|
* Build the served policy: the platform floor with project sources merged in.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/security/http/response/security-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"security-handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/security/http/response/security-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAgBzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AASjD;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,mTAmBjD,CAAC;AAqBF,wBAAgB,aAAa,IAAI,MAAM,CAItC;AAsLD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,EAC9B,OAAO,CAAC,EAAE,cAAc,EACxB,iBAAiB,CAAC,EAAE,OAAO,GAC1B,MAAM,CAgBR;AA0CD,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,EAC9B,OAAO,CAAC,EAAE,cAAc,GACvB,MAAM,CAMR;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,EAC9B,OAAO,CAAC,EAAE,cAAc,EACxB,iBAAiB,CAAC,EAAE,OAAO,GAC1B,IAAI,CAoHN"}
|
|
@@ -3,6 +3,7 @@ import { recordSecurityHeaders } from "../../../observability/index.js";
|
|
|
3
3
|
import { HOSTED_STUDIO_ORIGINS } from "../studio-origin-policy.js";
|
|
4
4
|
import { isCorsPolicyResponseHeaderName } from "../../../utils/cors-policy-limits.js";
|
|
5
5
|
import { serverLogger } from "../../../utils/logger/logger.js";
|
|
6
|
+
import { CSP_REPORT_ENDPOINT_NAME, CSP_REPORT_PATH, } from "../csp-report-endpoint.js";
|
|
6
7
|
import { PLATFORM_FONT_FILE_ORIGINS, PLATFORM_FONT_STYLE_ORIGINS, PLATFORM_IMAGE_ORIGINS, PLATFORM_SCRIPT_ORIGINS, } from "../platform-asset-origins.js";
|
|
7
8
|
import { toCspDirectiveName } from "../csp-directives.js";
|
|
8
9
|
const logger = serverLogger.component("security-headers");
|
|
@@ -26,15 +27,16 @@ export const SECURITY_POLICY_RESPONSE_HEADER_NAMES = Object.freeze([
|
|
|
26
27
|
"cross-origin-opener-policy",
|
|
27
28
|
"cross-origin-resource-policy",
|
|
28
29
|
"referrer-policy",
|
|
30
|
+
// Names where violation reports go. A project-provided value would send
|
|
31
|
+
// them somewhere else, or nowhere, and the policy would look healthy.
|
|
32
|
+
"reporting-endpoints",
|
|
29
33
|
"strict-transport-security",
|
|
30
34
|
"x-content-type-options",
|
|
31
35
|
"x-frame-options",
|
|
32
36
|
"x-xss-protection",
|
|
33
37
|
]);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return SECURITY_POLICY_RESPONSE_HEADER_NAME_SET.has(name.toLowerCase());
|
|
37
|
-
}
|
|
38
|
+
/** Response header defining the reporting groups the policy refers to. */
|
|
39
|
+
const REPORTING_ENDPOINTS_HEADER = "reporting-endpoints";
|
|
38
40
|
/** The two names the computed policy may be delivered under. */
|
|
39
41
|
const CSP_RESPONSE_HEADER_NAMES = new Set([
|
|
40
42
|
"content-security-policy",
|
|
@@ -114,6 +116,10 @@ function requiredDirectives(nonce, isVeryfrontDomain) {
|
|
|
114
116
|
"frame-ancestors": isVeryfrontDomain ? [...VERYFRONT_FRAME_ANCESTORS] : ["'none'"],
|
|
115
117
|
"base-uri": ["'self'"],
|
|
116
118
|
"form-action": ["'self'"],
|
|
119
|
+
// Both spellings: `report-to` is the current one, `report-uri` is
|
|
120
|
+
// deprecated but still the only one several shipping browsers honour.
|
|
121
|
+
"report-to": [CSP_REPORT_ENDPOINT_NAME],
|
|
122
|
+
"report-uri": [CSP_REPORT_PATH],
|
|
117
123
|
};
|
|
118
124
|
}
|
|
119
125
|
/**
|
|
@@ -149,7 +155,7 @@ function normalizeSources(sources) {
|
|
|
149
155
|
* directive but never the required class, which is how a project hardens past
|
|
150
156
|
* the floor without being able to break its own site.
|
|
151
157
|
*/
|
|
152
|
-
function mergeCspDirectives(required, projectCsp, nonce) {
|
|
158
|
+
function mergeCspDirectives(required, projectCsp, nonce, derived) {
|
|
153
159
|
const project = new Map();
|
|
154
160
|
for (const [key, value] of Object.entries(projectCsp ?? {})) {
|
|
155
161
|
if (value === undefined)
|
|
@@ -177,16 +183,23 @@ function mergeCspDirectives(required, projectCsp, nonce) {
|
|
|
177
183
|
const names = new Set([
|
|
178
184
|
...requiredByName.keys(),
|
|
179
185
|
...BASELINE_DIRECTIVES.keys(),
|
|
186
|
+
...Object.keys(derived ?? {}),
|
|
180
187
|
...project.keys(),
|
|
181
188
|
]);
|
|
182
189
|
const merged = Object.create(null);
|
|
183
190
|
for (const name of names) {
|
|
184
191
|
const configured = project.get(name);
|
|
185
192
|
const baseline = configured === null ? [] : BASELINE_DIRECTIVES.get(name) ?? [];
|
|
193
|
+
// Derived origins sit above the floor and below project config, and follow
|
|
194
|
+
// the baseline's `null` semantics: a project that explicitly drops a
|
|
195
|
+
// directive means it, and static analysis must not put back what the
|
|
196
|
+
// project just said it does not want.
|
|
197
|
+
const derivedForName = configured === null ? [] : derived?.[name] ?? [];
|
|
186
198
|
const additions = configured ?? [];
|
|
187
199
|
merged[name] = normalizeSources([
|
|
188
200
|
...(requiredByName.get(name) ?? []),
|
|
189
201
|
...baseline,
|
|
202
|
+
...derivedForName,
|
|
190
203
|
...additions,
|
|
191
204
|
]);
|
|
192
205
|
}
|
|
@@ -219,7 +232,7 @@ export function buildCSP(isDev, nonce, config, adapter, isVeryfrontDomain) {
|
|
|
219
232
|
// development allowance can never widen the production policy.
|
|
220
233
|
if (isDev)
|
|
221
234
|
return "";
|
|
222
|
-
return serializeDirectives(mergeCspDirectives(requiredDirectives(nonce, isVeryfrontDomain ?? false), config?.csp, nonce));
|
|
235
|
+
return serializeDirectives(mergeCspDirectives(requiredDirectives(nonce, isVeryfrontDomain ?? false), config?.csp, nonce, config?.derivedCsp));
|
|
223
236
|
}
|
|
224
237
|
/**
|
|
225
238
|
* Whether the built policy is enforced or merely reported.
|
|
@@ -290,6 +303,9 @@ export function applySecurityHeaders(headers, isDev, nonce, config, adapter, isV
|
|
|
290
303
|
if (csp) {
|
|
291
304
|
const hasEnvOverride = Boolean(adapter?.env?.get?.("VERYFRONT_CSP")?.trim());
|
|
292
305
|
headers.set(cspHeaderName(config, adapter, hasEnvOverride), csp);
|
|
306
|
+
// Names the group the policy's `report-to` refers to. Without this header
|
|
307
|
+
// the directive names nothing and the browser sends no reports at all.
|
|
308
|
+
headers.set(REPORTING_ENDPOINTS_HEADER, `${CSP_REPORT_ENDPOINT_NAME}="${CSP_REPORT_PATH}"`);
|
|
293
309
|
}
|
|
294
310
|
if (!isDev) {
|
|
295
311
|
const hstsMaxAge = config?.hsts?.maxAge ?? HSTS_MAX_AGE_SECONDS;
|
|
@@ -328,9 +344,9 @@ export function applySecurityHeaders(headers, isDev, nonce, config, adapter, isV
|
|
|
328
344
|
// `security.headers` would silently replace the merged platform floor --
|
|
329
345
|
// and, now that the floor may be delivered report-only, could also flip
|
|
330
346
|
// which mode is served. Header names are case-insensitive, so match that
|
|
331
|
-
// way.
|
|
332
|
-
|
|
333
|
-
if (isCspResponseHeaderName(
|
|
347
|
+
// way.
|
|
348
|
+
const headerName = key.toLowerCase();
|
|
349
|
+
if (isCspResponseHeaderName(headerName) || headerName === REPORTING_ENDPOINTS_HEADER) {
|
|
334
350
|
ignoredCspHeader = true;
|
|
335
351
|
continue;
|
|
336
352
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/src/security/sandbox/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C,eAAO,MAAM,qBAAqB,UAAY,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface SandboxOptions {
|
|
2
|
+
timeoutMs?: number;
|
|
3
|
+
memoryLimitMb?: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Operator opt-in env var that allows {@link runInWorker} to execute on Node.js
|
|
7
|
+
* despite the lack of permission isolation. Read via {@link getHostEnv} so that
|
|
8
|
+
* a per-request project env overlay cannot enable unsafe execution from inside
|
|
9
|
+
* a tenant context.
|
|
10
|
+
*
|
|
11
|
+
* @internal Exported for testing.
|
|
12
|
+
*/
|
|
13
|
+
export declare const NODE_SANDBOX_ALLOW_UNSAFE_ENV = "VERYFRONT_NODE_SANDBOX_ALLOW_UNSAFE";
|
|
14
|
+
/**
|
|
15
|
+
* Pure decision helper for the Node.js sandbox guard. Given the raw env-var
|
|
16
|
+
* value, decide whether to block execution on Node.
|
|
17
|
+
*
|
|
18
|
+
* Returns `true` only when the value is the literal string `"1"`. Any other
|
|
19
|
+
* value (including `"true"`, `"yes"`, whitespace, or empty string) keeps the
|
|
20
|
+
* guard active. Strict equality is intentional for a security opt-in.
|
|
21
|
+
*
|
|
22
|
+
* @internal Exported for testing only.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isNodeSandboxAllowedUnsafe(envValue: string | undefined): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Operator opt-in env var that allows {@link runInWorker} to execute on Bun
|
|
27
|
+
* despite the lack of permission isolation. Read via {@link getHostEnv} so that
|
|
28
|
+
* a per-request project env overlay cannot enable unsafe execution from inside
|
|
29
|
+
* a tenant context.
|
|
30
|
+
*
|
|
31
|
+
* @internal Exported for testing.
|
|
32
|
+
*/
|
|
33
|
+
export declare const BUN_SANDBOX_ALLOW_UNSAFE_ENV = "VERYFRONT_BUN_SANDBOX_ALLOW_UNSAFE";
|
|
34
|
+
/**
|
|
35
|
+
* Pure decision helper for the Bun sandbox guard. Given the raw env-var
|
|
36
|
+
* value, decide whether to block execution on Bun.
|
|
37
|
+
*
|
|
38
|
+
* Returns `true` only when the value is the literal string `"1"`. Any other
|
|
39
|
+
* value (including `"true"`, `"yes"`, whitespace, or empty string) keeps the
|
|
40
|
+
* guard active. Strict equality is intentional for a security opt-in.
|
|
41
|
+
*
|
|
42
|
+
* @internal Exported for testing only.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isBunSandboxAllowedUnsafe(envValue: string | undefined): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Run untrusted JavaScript in an isolated Worker.
|
|
47
|
+
*
|
|
48
|
+
* ## Isolation model
|
|
49
|
+
*
|
|
50
|
+
* - **Deno (recommended):** the worker is spawned with `permissions: "none"`,
|
|
51
|
+
* denying filesystem, network, env, and subprocess access. This is the
|
|
52
|
+
* primary safe execution path.
|
|
53
|
+
* - **Node.js:** Node Workers do not support permission isolation. They
|
|
54
|
+
* inherit full access to the filesystem, network, env vars, and built-in
|
|
55
|
+
* modules. Only memory limits can be enforced. Because of this, the Node
|
|
56
|
+
* path is **disabled by default** and throws {@link NOT_SUPPORTED}.
|
|
57
|
+
* Operators who deliberately trust their input may set the env var
|
|
58
|
+
* `VERYFRONT_NODE_SANDBOX_ALLOW_UNSAFE=1` to bypass the guard.
|
|
59
|
+
* - **Bun:** Bun Workers have the same lack of permission isolation as Node.js.
|
|
60
|
+
* The Bun path is **disabled by default** and throws {@link NOT_SUPPORTED}.
|
|
61
|
+
* Operators who deliberately trust their input may set the env var
|
|
62
|
+
* `VERYFRONT_BUN_SANDBOX_ALLOW_UNSAFE=1` to bypass the guard.
|
|
63
|
+
*
|
|
64
|
+
* Callers SHOULD NOT pass untrusted code to this function on Node.js or Bun
|
|
65
|
+
* unless they have audited every caller and operator and accept the risk.
|
|
66
|
+
*
|
|
67
|
+
* @throws NOT_SUPPORTED on Node.js without the explicit opt-in env var.
|
|
68
|
+
* @throws NOT_SUPPORTED on Bun without the explicit opt-in env var.
|
|
69
|
+
*/
|
|
70
|
+
export declare function runInWorker<T = unknown>(code: string, options?: SandboxOptions): Promise<T>;
|
|
71
|
+
//# sourceMappingURL=deno-sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deno-sandbox.d.ts","sourceRoot":"","sources":["../../../../src/src/security/sandbox/deno-sandbox.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAYD;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,wCAAwC,CAAC;AAEnF;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEhF;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,uCAAuC,CAAC;AAEjF;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE/E;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CA6H/F"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { DEFAULT_SANDBOX_TIMEOUT_MS, MAX_SANDBOX_CODE_SIZE } from "./constants.js";
|
|
2
|
+
import { isCompiledBinary, serverLogger } from "../../utils/index.js";
|
|
3
|
+
import { isBun, isDeno, isNode } from "../../platform/compat/runtime.js";
|
|
4
|
+
import { getHostEnv } from "../../platform/compat/process/env.js";
|
|
5
|
+
import { withSpan } from "../../observability/tracing/otlp-setup.js";
|
|
6
|
+
import { INVALID_ARGUMENT, NOT_SUPPORTED, TIMEOUT_ERROR, UNKNOWN_ERROR } from "../../errors/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Operator opt-in env var that allows {@link runInWorker} to execute on Node.js
|
|
9
|
+
* despite the lack of permission isolation. Read via {@link getHostEnv} so that
|
|
10
|
+
* a per-request project env overlay cannot enable unsafe execution from inside
|
|
11
|
+
* a tenant context.
|
|
12
|
+
*
|
|
13
|
+
* @internal Exported for testing.
|
|
14
|
+
*/
|
|
15
|
+
export const NODE_SANDBOX_ALLOW_UNSAFE_ENV = "VERYFRONT_NODE_SANDBOX_ALLOW_UNSAFE";
|
|
16
|
+
/**
|
|
17
|
+
* Pure decision helper for the Node.js sandbox guard. Given the raw env-var
|
|
18
|
+
* value, decide whether to block execution on Node.
|
|
19
|
+
*
|
|
20
|
+
* Returns `true` only when the value is the literal string `"1"`. Any other
|
|
21
|
+
* value (including `"true"`, `"yes"`, whitespace, or empty string) keeps the
|
|
22
|
+
* guard active. Strict equality is intentional for a security opt-in.
|
|
23
|
+
*
|
|
24
|
+
* @internal Exported for testing only.
|
|
25
|
+
*/
|
|
26
|
+
export function isNodeSandboxAllowedUnsafe(envValue) {
|
|
27
|
+
return envValue === "1";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Operator opt-in env var that allows {@link runInWorker} to execute on Bun
|
|
31
|
+
* despite the lack of permission isolation. Read via {@link getHostEnv} so that
|
|
32
|
+
* a per-request project env overlay cannot enable unsafe execution from inside
|
|
33
|
+
* a tenant context.
|
|
34
|
+
*
|
|
35
|
+
* @internal Exported for testing.
|
|
36
|
+
*/
|
|
37
|
+
export const BUN_SANDBOX_ALLOW_UNSAFE_ENV = "VERYFRONT_BUN_SANDBOX_ALLOW_UNSAFE";
|
|
38
|
+
/**
|
|
39
|
+
* Pure decision helper for the Bun sandbox guard. Given the raw env-var
|
|
40
|
+
* value, decide whether to block execution on Bun.
|
|
41
|
+
*
|
|
42
|
+
* Returns `true` only when the value is the literal string `"1"`. Any other
|
|
43
|
+
* value (including `"true"`, `"yes"`, whitespace, or empty string) keeps the
|
|
44
|
+
* guard active. Strict equality is intentional for a security opt-in.
|
|
45
|
+
*
|
|
46
|
+
* @internal Exported for testing only.
|
|
47
|
+
*/
|
|
48
|
+
export function isBunSandboxAllowedUnsafe(envValue) {
|
|
49
|
+
return envValue === "1";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run untrusted JavaScript in an isolated Worker.
|
|
53
|
+
*
|
|
54
|
+
* ## Isolation model
|
|
55
|
+
*
|
|
56
|
+
* - **Deno (recommended):** the worker is spawned with `permissions: "none"`,
|
|
57
|
+
* denying filesystem, network, env, and subprocess access. This is the
|
|
58
|
+
* primary safe execution path.
|
|
59
|
+
* - **Node.js:** Node Workers do not support permission isolation. They
|
|
60
|
+
* inherit full access to the filesystem, network, env vars, and built-in
|
|
61
|
+
* modules. Only memory limits can be enforced. Because of this, the Node
|
|
62
|
+
* path is **disabled by default** and throws {@link NOT_SUPPORTED}.
|
|
63
|
+
* Operators who deliberately trust their input may set the env var
|
|
64
|
+
* `VERYFRONT_NODE_SANDBOX_ALLOW_UNSAFE=1` to bypass the guard.
|
|
65
|
+
* - **Bun:** Bun Workers have the same lack of permission isolation as Node.js.
|
|
66
|
+
* The Bun path is **disabled by default** and throws {@link NOT_SUPPORTED}.
|
|
67
|
+
* Operators who deliberately trust their input may set the env var
|
|
68
|
+
* `VERYFRONT_BUN_SANDBOX_ALLOW_UNSAFE=1` to bypass the guard.
|
|
69
|
+
*
|
|
70
|
+
* Callers SHOULD NOT pass untrusted code to this function on Node.js or Bun
|
|
71
|
+
* unless they have audited every caller and operator and accept the risk.
|
|
72
|
+
*
|
|
73
|
+
* @throws NOT_SUPPORTED on Node.js without the explicit opt-in env var.
|
|
74
|
+
* @throws NOT_SUPPORTED on Bun without the explicit opt-in env var.
|
|
75
|
+
*/
|
|
76
|
+
export function runInWorker(code, options = {}) {
|
|
77
|
+
if (typeof code !== "string") {
|
|
78
|
+
return Promise.reject(INVALID_ARGUMENT.create({ message: "Sandbox code must be a string" }));
|
|
79
|
+
}
|
|
80
|
+
if (code.length === 0) {
|
|
81
|
+
return Promise.reject(INVALID_ARGUMENT.create({ message: "Sandbox code cannot be empty" }));
|
|
82
|
+
}
|
|
83
|
+
const codeByteLength = new TextEncoder().encode(code).byteLength;
|
|
84
|
+
if (codeByteLength > MAX_SANDBOX_CODE_SIZE) {
|
|
85
|
+
return Promise.reject(INVALID_ARGUMENT.create({
|
|
86
|
+
message: `Sandbox code exceeds maximum size (${MAX_SANDBOX_CODE_SIZE} bytes)`,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
// SEC-008: Node Workers have no permission isolation. Refuse execution
|
|
90
|
+
// unless the operator has explicitly opted in via host env var. Use
|
|
91
|
+
// getHostEnv so a tenant project env overlay cannot enable this.
|
|
92
|
+
if (isNode && !isNodeSandboxAllowedUnsafe(getHostEnv(NODE_SANDBOX_ALLOW_UNSAFE_ENV))) {
|
|
93
|
+
return Promise.reject(NOT_SUPPORTED.create({
|
|
94
|
+
detail: "Sandbox execution is not safely supported on Node.js. The current " +
|
|
95
|
+
"implementation provides only memory limits, not permission isolation. " +
|
|
96
|
+
"Set VERYFRONT_NODE_SANDBOX_ALLOW_UNSAFE=1 to enable execution at your " +
|
|
97
|
+
"own risk, or run under Deno for full isolation.",
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
// SEC-008: Bun Workers have no permission isolation (same risk as Node.js).
|
|
101
|
+
// Refuse execution unless the operator has explicitly opted in via host env
|
|
102
|
+
// var. Use getHostEnv so a tenant project env overlay cannot enable this.
|
|
103
|
+
if (isBun && !isBunSandboxAllowedUnsafe(getHostEnv(BUN_SANDBOX_ALLOW_UNSAFE_ENV))) {
|
|
104
|
+
return Promise.reject(NOT_SUPPORTED.create({
|
|
105
|
+
detail: "Sandbox execution is not safely supported on Bun. The current " +
|
|
106
|
+
"implementation provides no permission isolation. " +
|
|
107
|
+
"Set VERYFRONT_BUN_SANDBOX_ALLOW_UNSAFE=1 to enable execution at your " +
|
|
108
|
+
"own risk, or run under Deno for full isolation.",
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS;
|
|
112
|
+
return withSpan("security.sandbox.runInWorker", () => {
|
|
113
|
+
const workerOptions = { type: "module" };
|
|
114
|
+
if (isDeno)
|
|
115
|
+
workerOptions.deno = { permissions: "none" };
|
|
116
|
+
const { memoryLimitMb } = options;
|
|
117
|
+
if (typeof memoryLimitMb === "number") {
|
|
118
|
+
if (!Number.isFinite(memoryLimitMb) || memoryLimitMb <= 0) {
|
|
119
|
+
throw INVALID_ARGUMENT.create({
|
|
120
|
+
detail: "Sandbox memoryLimitMb must be a positive, finite number",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (!isNode) {
|
|
124
|
+
throw NOT_SUPPORTED.create({
|
|
125
|
+
detail: "Sandbox memory limits are not supported in this runtime",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
workerOptions.resourceLimits = {
|
|
129
|
+
...workerOptions.resourceLimits,
|
|
130
|
+
maxOldGenerationSizeMb: Math.floor(memoryLimitMb),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const workerCode = `self.onmessage = async (e) => {` +
|
|
134
|
+
` const { code } = e.data;` +
|
|
135
|
+
` let result;` +
|
|
136
|
+
` try { result = await (async () => {` +
|
|
137
|
+
` return await (new Function(code))();` +
|
|
138
|
+
` })(); } catch (error) {` +
|
|
139
|
+
` self.postMessage({ error: String(error && error.message || error) });` +
|
|
140
|
+
` return;` +
|
|
141
|
+
` }` +
|
|
142
|
+
` self.postMessage({ result });` +
|
|
143
|
+
`};`;
|
|
144
|
+
// Use data URL for compiled binaries (blob URLs don't work in deno compile)
|
|
145
|
+
// See: https://github.com/denoland/deno/issues/18327
|
|
146
|
+
const workerUrl = isCompiledBinary()
|
|
147
|
+
? `data:text/javascript;base64,${btoa(workerCode)}`
|
|
148
|
+
: URL.createObjectURL(new Blob([workerCode], { type: "application/javascript" }));
|
|
149
|
+
const worker = new Worker(workerUrl, workerOptions);
|
|
150
|
+
function safeTerminate(logMessage, error) {
|
|
151
|
+
try {
|
|
152
|
+
worker.terminate();
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
serverLogger.debug(logMessage, { error: error ?? e });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const promise = new Promise((resolve, reject) => {
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
safeTerminate("[sandbox] worker terminate failed");
|
|
161
|
+
reject(TIMEOUT_ERROR.create({ detail: "Sandbox timeout" }));
|
|
162
|
+
}, timeoutMs);
|
|
163
|
+
worker.onmessage = (e) => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
const { result, error } = e.data ?? {};
|
|
166
|
+
if (error)
|
|
167
|
+
reject(UNKNOWN_ERROR.create({ detail: error }));
|
|
168
|
+
else
|
|
169
|
+
resolve(result);
|
|
170
|
+
safeTerminate("[sandbox] worker terminate failed");
|
|
171
|
+
};
|
|
172
|
+
worker.onerror = (e) => {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
reject(UNKNOWN_ERROR.create({ detail: String(e.message || e.error || "Worker error") }));
|
|
175
|
+
safeTerminate("[sandbox] worker terminate failed on error");
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
worker.postMessage({ code });
|
|
179
|
+
return promise;
|
|
180
|
+
}, {
|
|
181
|
+
"sandbox.timeoutMs": timeoutMs,
|
|
182
|
+
"sandbox.memoryLimitMb": options.memoryLimitMb ?? 0,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Permission = "net" | "fs" | "env" | "run" | "read" | "write";
|
|
2
|
+
export interface PermissionRequest {
|
|
3
|
+
name: Permission;
|
|
4
|
+
host?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PermissionResult {
|
|
8
|
+
state: "granted" | "denied" | "prompt";
|
|
9
|
+
}
|
|
10
|
+
export declare function requestPermission(request: PermissionRequest): Promise<PermissionResult>;
|
|
11
|
+
//# sourceMappingURL=permission-system.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-system.d.ts","sourceRoot":"","sources":["../../../../src/src/security/sandbox/permission-system.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACxC;AAkGD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as dntShim from "../../../_dnt.shims.js";
|
|
2
|
+
import { serverLogger } from "../../utils/index.js";
|
|
3
|
+
import { isDeno } from "../../platform/compat/runtime.js";
|
|
4
|
+
import { withSpan } from "../../observability/tracing/otlp-setup.js";
|
|
5
|
+
const logger = serverLogger.component("permissions");
|
|
6
|
+
/**
|
|
7
|
+
* Access the Deno permissions API through a narrow, typed view.
|
|
8
|
+
* Returns null when not running under Deno or the API is unavailable.
|
|
9
|
+
*/
|
|
10
|
+
function getDenoPermissions() {
|
|
11
|
+
const denoGlobal = dntShim.dntGlobalThis.Deno;
|
|
12
|
+
const permissions = denoGlobal?.permissions;
|
|
13
|
+
if (!permissions || typeof permissions.request !== "function")
|
|
14
|
+
return null;
|
|
15
|
+
return permissions;
|
|
16
|
+
}
|
|
17
|
+
function createPermissionDescriptor(request) {
|
|
18
|
+
const { name, host, path } = request;
|
|
19
|
+
switch (name) {
|
|
20
|
+
case "net":
|
|
21
|
+
return host ? { name, host } : { name };
|
|
22
|
+
case "read":
|
|
23
|
+
case "write":
|
|
24
|
+
return path ? { name, path } : { name };
|
|
25
|
+
case "env":
|
|
26
|
+
case "run":
|
|
27
|
+
return { name };
|
|
28
|
+
case "fs":
|
|
29
|
+
default:
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function requestDenoPermission(request) {
|
|
34
|
+
return withSpan("security.permissions.requestDeno", async () => {
|
|
35
|
+
const permissions = getDenoPermissions();
|
|
36
|
+
if (!isDeno || !permissions)
|
|
37
|
+
return { state: "denied" };
|
|
38
|
+
if (request.name === "fs") {
|
|
39
|
+
const path = request.path;
|
|
40
|
+
const readStatus = await permissions.request({ name: "read", path });
|
|
41
|
+
if (readStatus.state !== "granted")
|
|
42
|
+
return { state: readStatus.state };
|
|
43
|
+
const writeStatus = await permissions.request({ name: "write", path });
|
|
44
|
+
return { state: writeStatus.state };
|
|
45
|
+
}
|
|
46
|
+
const descriptor = createPermissionDescriptor(request);
|
|
47
|
+
if (!descriptor) {
|
|
48
|
+
logger.warn("Unsupported permission request", request);
|
|
49
|
+
return { state: "denied" };
|
|
50
|
+
}
|
|
51
|
+
const status = await permissions.request(descriptor);
|
|
52
|
+
return { state: status.state };
|
|
53
|
+
}, { "permission.name": request.name });
|
|
54
|
+
}
|
|
55
|
+
let warnedNonDenoAutoGrant = false;
|
|
56
|
+
function warnNonDenoAutoGrantOnce() {
|
|
57
|
+
if (warnedNonDenoAutoGrant)
|
|
58
|
+
return;
|
|
59
|
+
warnedNonDenoAutoGrant = true;
|
|
60
|
+
logger.warn("Permission sandbox is a no-op on non-Deno runtimes: all permission " +
|
|
61
|
+
"requests are auto-granted. Do not rely on this guard for isolation " +
|
|
62
|
+
"when running outside Deno.");
|
|
63
|
+
}
|
|
64
|
+
export function requestPermission(request) {
|
|
65
|
+
return withSpan("security.permissions.request", async () => {
|
|
66
|
+
try {
|
|
67
|
+
if (isDeno)
|
|
68
|
+
return await requestDenoPermission(request);
|
|
69
|
+
// On non-Deno runtimes there is no OS-level permission API to consult,
|
|
70
|
+
// so every request is auto-granted — this guard provides NO isolation
|
|
71
|
+
// here. Warn loudly (once) so an operator running the runtime outside
|
|
72
|
+
// Deno cannot mistake this for an enforced sandbox.
|
|
73
|
+
warnNonDenoAutoGrantOnce();
|
|
74
|
+
serverLogger.debug("[permissions] Permission auto-granted (non-Deno runtime)", { permission: request.name });
|
|
75
|
+
return { state: "granted" };
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
logger.warn("Permission request failed", {
|
|
79
|
+
permission: request.name,
|
|
80
|
+
error,
|
|
81
|
+
});
|
|
82
|
+
return { state: "denied" };
|
|
83
|
+
}
|
|
84
|
+
}, { "permission.name": request.name });
|
|
85
|
+
}
|