@renderify/security 0.1.0
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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/security.cjs.js +582 -0
- package/dist/security.cjs.js.map +1 -0
- package/dist/security.d.mts +68 -0
- package/dist/security.d.ts +68 -0
- package/dist/security.esm.js +567 -0
- package/dist/security.esm.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Web LLM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @renderify/security
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Security policy engine for Renderify RuntimePlan execution.
|
|
8
|
+
|
|
9
|
+
`@renderify/security` validates plans, capabilities, module sources, and runtime source code before execution.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @renderify/security @renderify/ir
|
|
15
|
+
# or
|
|
16
|
+
npm i @renderify/security @renderify/ir
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Main API
|
|
20
|
+
|
|
21
|
+
- `DefaultSecurityChecker`
|
|
22
|
+
- `listSecurityProfiles()`
|
|
23
|
+
- `getSecurityProfilePolicy(profile)`
|
|
24
|
+
- Types: `RuntimeSecurityPolicy`, `SecurityCheckResult`, `RuntimeSecurityProfile`
|
|
25
|
+
|
|
26
|
+
## Profiles
|
|
27
|
+
|
|
28
|
+
- `strict`
|
|
29
|
+
- `balanced` (default)
|
|
30
|
+
- `relaxed`
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { DefaultSecurityChecker } from "@renderify/security";
|
|
36
|
+
|
|
37
|
+
const checker = new DefaultSecurityChecker();
|
|
38
|
+
checker.initialize({ profile: "strict" });
|
|
39
|
+
|
|
40
|
+
const result = await checker.checkPlan(plan);
|
|
41
|
+
if (!result.safe) {
|
|
42
|
+
console.error(result.issues);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What It Checks
|
|
47
|
+
|
|
48
|
+
- Blocked HTML tags and tree limits
|
|
49
|
+
- Allowed module specifiers and network hosts
|
|
50
|
+
- Execution profile limits and capability quotas
|
|
51
|
+
- Runtime source module constraints
|
|
52
|
+
- Spec version and module manifest requirements
|
|
53
|
+
|
|
54
|
+
## Notes
|
|
55
|
+
|
|
56
|
+
- `checkPlan()` is async.
|
|
57
|
+
- Use policy overrides via `initialize({ profile, overrides })` for environment-specific constraints.
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
DefaultSecurityChecker: () => DefaultSecurityChecker,
|
|
24
|
+
getSecurityProfilePolicy: () => getSecurityProfilePolicy,
|
|
25
|
+
listSecurityProfiles: () => listSecurityProfiles
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
var import_ir = require("@renderify/ir");
|
|
29
|
+
var SECURITY_PROFILE_POLICIES = {
|
|
30
|
+
strict: {
|
|
31
|
+
blockedTags: ["script", "iframe", "object", "embed", "link", "meta"],
|
|
32
|
+
maxTreeDepth: 8,
|
|
33
|
+
maxNodeCount: 250,
|
|
34
|
+
allowInlineEventHandlers: false,
|
|
35
|
+
allowedModules: ["/", "npm:"],
|
|
36
|
+
allowedNetworkHosts: ["ga.jspm.io", "cdn.jspm.io"],
|
|
37
|
+
allowArbitraryNetwork: false,
|
|
38
|
+
allowedExecutionProfiles: [
|
|
39
|
+
"standard",
|
|
40
|
+
"isolated-vm",
|
|
41
|
+
"sandbox-worker",
|
|
42
|
+
"sandbox-iframe"
|
|
43
|
+
],
|
|
44
|
+
maxTransitionsPerPlan: 40,
|
|
45
|
+
maxActionsPerTransition: 20,
|
|
46
|
+
maxAllowedImports: 80,
|
|
47
|
+
maxAllowedExecutionMs: 5e3,
|
|
48
|
+
maxAllowedComponentInvocations: 120,
|
|
49
|
+
allowRuntimeSourceModules: true,
|
|
50
|
+
maxRuntimeSourceBytes: 2e4,
|
|
51
|
+
supportedSpecVersions: [import_ir.DEFAULT_RUNTIME_PLAN_SPEC_VERSION],
|
|
52
|
+
requireSpecVersion: true,
|
|
53
|
+
requireModuleManifestForBareSpecifiers: true,
|
|
54
|
+
requireModuleIntegrity: true,
|
|
55
|
+
allowDynamicSourceImports: false,
|
|
56
|
+
sourceBannedPatternStrings: [
|
|
57
|
+
"\\beval\\s*\\(",
|
|
58
|
+
"\\bnew\\s+Function\\b",
|
|
59
|
+
"\\bfetch\\s*\\(",
|
|
60
|
+
"\\bXMLHttpRequest\\b",
|
|
61
|
+
"\\bWebSocket\\b",
|
|
62
|
+
"\\bimportScripts\\b",
|
|
63
|
+
"\\bdocument\\s*\\.\\s*cookie\\b",
|
|
64
|
+
"\\blocalStorage\\b",
|
|
65
|
+
"\\bsessionStorage\\b",
|
|
66
|
+
"\\bindexedDB\\b",
|
|
67
|
+
"\\bnavigator\\s*\\.\\s*sendBeacon\\b",
|
|
68
|
+
"\\bchild_process\\b",
|
|
69
|
+
"\\bprocess\\s*\\.\\s*env\\b"
|
|
70
|
+
],
|
|
71
|
+
maxSourceImportSpecifiers: 30
|
|
72
|
+
},
|
|
73
|
+
balanced: {
|
|
74
|
+
blockedTags: ["script", "iframe", "object", "embed", "link", "meta"],
|
|
75
|
+
maxTreeDepth: 12,
|
|
76
|
+
maxNodeCount: 500,
|
|
77
|
+
allowInlineEventHandlers: false,
|
|
78
|
+
allowedModules: ["/", "npm:"],
|
|
79
|
+
allowedNetworkHosts: ["ga.jspm.io", "cdn.jspm.io"],
|
|
80
|
+
allowArbitraryNetwork: false,
|
|
81
|
+
allowedExecutionProfiles: [
|
|
82
|
+
"standard",
|
|
83
|
+
"isolated-vm",
|
|
84
|
+
"sandbox-worker",
|
|
85
|
+
"sandbox-iframe"
|
|
86
|
+
],
|
|
87
|
+
maxTransitionsPerPlan: 100,
|
|
88
|
+
maxActionsPerTransition: 50,
|
|
89
|
+
maxAllowedImports: 200,
|
|
90
|
+
maxAllowedExecutionMs: 15e3,
|
|
91
|
+
maxAllowedComponentInvocations: 500,
|
|
92
|
+
allowRuntimeSourceModules: true,
|
|
93
|
+
maxRuntimeSourceBytes: 8e4,
|
|
94
|
+
supportedSpecVersions: [import_ir.DEFAULT_RUNTIME_PLAN_SPEC_VERSION],
|
|
95
|
+
requireSpecVersion: true,
|
|
96
|
+
requireModuleManifestForBareSpecifiers: true,
|
|
97
|
+
requireModuleIntegrity: false,
|
|
98
|
+
allowDynamicSourceImports: false,
|
|
99
|
+
sourceBannedPatternStrings: [
|
|
100
|
+
"\\beval\\s*\\(",
|
|
101
|
+
"\\bnew\\s+Function\\b",
|
|
102
|
+
"\\bfetch\\s*\\(",
|
|
103
|
+
"\\bXMLHttpRequest\\b",
|
|
104
|
+
"\\bWebSocket\\b",
|
|
105
|
+
"\\bimportScripts\\b",
|
|
106
|
+
"\\bdocument\\s*\\.\\s*cookie\\b",
|
|
107
|
+
"\\blocalStorage\\b",
|
|
108
|
+
"\\bsessionStorage\\b",
|
|
109
|
+
"\\bchild_process\\b"
|
|
110
|
+
],
|
|
111
|
+
maxSourceImportSpecifiers: 120
|
|
112
|
+
},
|
|
113
|
+
relaxed: {
|
|
114
|
+
blockedTags: ["script", "iframe", "object", "embed"],
|
|
115
|
+
maxTreeDepth: 24,
|
|
116
|
+
maxNodeCount: 2e3,
|
|
117
|
+
allowInlineEventHandlers: true,
|
|
118
|
+
allowedModules: [
|
|
119
|
+
"/",
|
|
120
|
+
"npm:",
|
|
121
|
+
"https://ga.jspm.io/",
|
|
122
|
+
"https://cdn.jspm.io/"
|
|
123
|
+
],
|
|
124
|
+
allowedNetworkHosts: ["ga.jspm.io", "cdn.jspm.io", "esm.sh", "unpkg.com"],
|
|
125
|
+
allowArbitraryNetwork: true,
|
|
126
|
+
allowedExecutionProfiles: [
|
|
127
|
+
"standard",
|
|
128
|
+
"isolated-vm",
|
|
129
|
+
"sandbox-worker",
|
|
130
|
+
"sandbox-iframe"
|
|
131
|
+
],
|
|
132
|
+
maxTransitionsPerPlan: 400,
|
|
133
|
+
maxActionsPerTransition: 150,
|
|
134
|
+
maxAllowedImports: 1e3,
|
|
135
|
+
maxAllowedExecutionMs: 6e4,
|
|
136
|
+
maxAllowedComponentInvocations: 4e3,
|
|
137
|
+
allowRuntimeSourceModules: true,
|
|
138
|
+
maxRuntimeSourceBytes: 2e5,
|
|
139
|
+
supportedSpecVersions: [import_ir.DEFAULT_RUNTIME_PLAN_SPEC_VERSION],
|
|
140
|
+
requireSpecVersion: false,
|
|
141
|
+
requireModuleManifestForBareSpecifiers: false,
|
|
142
|
+
requireModuleIntegrity: false,
|
|
143
|
+
allowDynamicSourceImports: true,
|
|
144
|
+
sourceBannedPatternStrings: ["\\bchild_process\\b"],
|
|
145
|
+
maxSourceImportSpecifiers: 500
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
var DEFAULT_SECURITY_PROFILE = "balanced";
|
|
149
|
+
function listSecurityProfiles() {
|
|
150
|
+
return Object.keys(SECURITY_PROFILE_POLICIES);
|
|
151
|
+
}
|
|
152
|
+
function getSecurityProfilePolicy(profile) {
|
|
153
|
+
return clonePolicy(SECURITY_PROFILE_POLICIES[profile]);
|
|
154
|
+
}
|
|
155
|
+
var DefaultSecurityChecker = class {
|
|
156
|
+
policy = getSecurityProfilePolicy(
|
|
157
|
+
DEFAULT_SECURITY_PROFILE
|
|
158
|
+
);
|
|
159
|
+
profile = DEFAULT_SECURITY_PROFILE;
|
|
160
|
+
initialize(input) {
|
|
161
|
+
const normalized = normalizeInitializationInput(input);
|
|
162
|
+
const profile = normalized.profile ?? DEFAULT_SECURITY_PROFILE;
|
|
163
|
+
const basePolicy = getSecurityProfilePolicy(profile);
|
|
164
|
+
this.policy = {
|
|
165
|
+
...basePolicy,
|
|
166
|
+
...normalized.overrides,
|
|
167
|
+
blockedTags: normalized.overrides?.blockedTags ?? basePolicy.blockedTags,
|
|
168
|
+
allowedModules: normalized.overrides?.allowedModules ?? basePolicy.allowedModules,
|
|
169
|
+
allowedNetworkHosts: normalized.overrides?.allowedNetworkHosts ?? basePolicy.allowedNetworkHosts,
|
|
170
|
+
allowedExecutionProfiles: normalized.overrides?.allowedExecutionProfiles ?? basePolicy.allowedExecutionProfiles,
|
|
171
|
+
supportedSpecVersions: normalized.overrides?.supportedSpecVersions ?? basePolicy.supportedSpecVersions,
|
|
172
|
+
sourceBannedPatternStrings: normalized.overrides?.sourceBannedPatternStrings ?? basePolicy.sourceBannedPatternStrings
|
|
173
|
+
};
|
|
174
|
+
this.profile = profile;
|
|
175
|
+
}
|
|
176
|
+
getPolicy() {
|
|
177
|
+
return { ...this.policy };
|
|
178
|
+
}
|
|
179
|
+
getProfile() {
|
|
180
|
+
return this.profile;
|
|
181
|
+
}
|
|
182
|
+
async checkPlan(plan) {
|
|
183
|
+
const issues = [];
|
|
184
|
+
const diagnostics = [];
|
|
185
|
+
const planSpecVersion = (0, import_ir.resolveRuntimePlanSpecVersion)(plan.specVersion);
|
|
186
|
+
const moduleManifest = plan.moduleManifest;
|
|
187
|
+
if (this.policy.requireSpecVersion && (typeof plan.specVersion !== "string" || plan.specVersion.trim().length === 0)) {
|
|
188
|
+
issues.push("Runtime plan specVersion is required by policy");
|
|
189
|
+
}
|
|
190
|
+
if (!this.policy.supportedSpecVersions.includes(planSpecVersion)) {
|
|
191
|
+
issues.push(
|
|
192
|
+
`Runtime plan specVersion ${planSpecVersion} is not supported by policy`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (moduleManifest) {
|
|
196
|
+
issues.push(...this.checkModuleManifest(moduleManifest));
|
|
197
|
+
}
|
|
198
|
+
if (this.policy.requireModuleManifestForBareSpecifiers) {
|
|
199
|
+
issues.push(...await this.checkManifestCoverage(plan, moduleManifest));
|
|
200
|
+
}
|
|
201
|
+
const capabilityResult = this.checkCapabilities(
|
|
202
|
+
plan.capabilities,
|
|
203
|
+
moduleManifest
|
|
204
|
+
);
|
|
205
|
+
issues.push(...capabilityResult.issues);
|
|
206
|
+
diagnostics.push(...capabilityResult.diagnostics);
|
|
207
|
+
let nodeCount = 0;
|
|
208
|
+
const walk = (node, depth) => {
|
|
209
|
+
nodeCount += 1;
|
|
210
|
+
if (depth > this.policy.maxTreeDepth) {
|
|
211
|
+
issues.push(
|
|
212
|
+
`Node depth ${depth} exceeds maximum ${this.policy.maxTreeDepth}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (node.type === "element") {
|
|
216
|
+
const normalizedTag = node.tag.toLowerCase();
|
|
217
|
+
if (this.policy.blockedTags.includes(normalizedTag)) {
|
|
218
|
+
issues.push(`Blocked tag detected: <${normalizedTag}>`);
|
|
219
|
+
}
|
|
220
|
+
if (node.props) {
|
|
221
|
+
for (const key of Object.keys(node.props)) {
|
|
222
|
+
if (!this.policy.allowInlineEventHandlers && /^on[A-Z]|^on[a-z]/.test(key)) {
|
|
223
|
+
issues.push(`Inline event handler is not allowed: ${key}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (node.type === "component") {
|
|
229
|
+
const componentSpecifier = this.resolveManifestSpecifier(
|
|
230
|
+
node.module,
|
|
231
|
+
moduleManifest
|
|
232
|
+
);
|
|
233
|
+
const componentResult = this.checkModuleSpecifier(componentSpecifier);
|
|
234
|
+
issues.push(...componentResult.issues);
|
|
235
|
+
}
|
|
236
|
+
if (node.type === "text") {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
for (const child of node.children ?? []) {
|
|
240
|
+
walk(child, depth + 1);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
walk(plan.root, 0);
|
|
244
|
+
if (nodeCount > this.policy.maxNodeCount) {
|
|
245
|
+
issues.push(
|
|
246
|
+
`Node count ${nodeCount} exceeds maximum ${this.policy.maxNodeCount}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const importSpecifiers = plan.imports ?? [];
|
|
250
|
+
for (const specifier of importSpecifiers) {
|
|
251
|
+
const effectiveSpecifier = this.resolveManifestSpecifier(
|
|
252
|
+
specifier,
|
|
253
|
+
moduleManifest
|
|
254
|
+
);
|
|
255
|
+
const importCheck = this.checkModuleSpecifier(effectiveSpecifier);
|
|
256
|
+
issues.push(...importCheck.issues);
|
|
257
|
+
}
|
|
258
|
+
if (plan.state) {
|
|
259
|
+
issues.push(...this.checkStateModel(plan.state));
|
|
260
|
+
}
|
|
261
|
+
if (plan.source) {
|
|
262
|
+
issues.push(
|
|
263
|
+
...await this.checkRuntimeSource(plan.source, moduleManifest)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
for (const issue of issues) {
|
|
267
|
+
diagnostics.push({
|
|
268
|
+
level: "error",
|
|
269
|
+
code: "SECURITY_POLICY_VIOLATION",
|
|
270
|
+
message: issue
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
safe: issues.length === 0,
|
|
275
|
+
issues,
|
|
276
|
+
diagnostics
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
checkModuleSpecifier(specifier) {
|
|
280
|
+
const issues = [];
|
|
281
|
+
const diagnostics = [];
|
|
282
|
+
if (specifier.includes("..")) {
|
|
283
|
+
issues.push(
|
|
284
|
+
`Path traversal is not allowed in module specifier: ${specifier}`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const isUrl = this.isUrl(specifier);
|
|
288
|
+
if (isUrl) {
|
|
289
|
+
const parsedUrl = new URL(specifier);
|
|
290
|
+
if (!this.policy.allowArbitraryNetwork && !this.policy.allowedNetworkHosts.includes(parsedUrl.host)) {
|
|
291
|
+
issues.push(`Network host is not in allowlist: ${parsedUrl.host}`);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
const allowed = this.policy.allowedModules.length === 0 || this.policy.allowedModules.some(
|
|
295
|
+
(prefix) => specifier.startsWith(prefix)
|
|
296
|
+
);
|
|
297
|
+
if (!allowed) {
|
|
298
|
+
issues.push(`Module specifier is not in allowlist: ${specifier}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const issue of issues) {
|
|
302
|
+
diagnostics.push({
|
|
303
|
+
level: "error",
|
|
304
|
+
code: "SECURITY_MODULE_REJECTED",
|
|
305
|
+
message: issue
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
safe: issues.length === 0,
|
|
310
|
+
issues,
|
|
311
|
+
diagnostics
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
checkCapabilities(capabilities, moduleManifest) {
|
|
315
|
+
const issues = [];
|
|
316
|
+
const diagnostics = [];
|
|
317
|
+
const requestedHosts = capabilities.networkHosts ?? [];
|
|
318
|
+
if (!this.policy.allowArbitraryNetwork) {
|
|
319
|
+
for (const host of requestedHosts) {
|
|
320
|
+
if (!this.policy.allowedNetworkHosts.includes(host)) {
|
|
321
|
+
issues.push(`Requested network host is not allowed: ${host}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const requestedModules = capabilities.allowedModules ?? [];
|
|
326
|
+
for (const moduleSpecifier of requestedModules) {
|
|
327
|
+
const effectiveSpecifier = this.resolveManifestSpecifier(
|
|
328
|
+
moduleSpecifier,
|
|
329
|
+
moduleManifest
|
|
330
|
+
);
|
|
331
|
+
const checkResult = this.checkModuleSpecifier(effectiveSpecifier);
|
|
332
|
+
issues.push(...checkResult.issues);
|
|
333
|
+
if (this.policy.requireModuleManifestForBareSpecifiers && this.isBareSpecifier(moduleSpecifier) && !moduleManifest?.[moduleSpecifier]) {
|
|
334
|
+
issues.push(
|
|
335
|
+
`Missing moduleManifest entry for bare specifier: ${moduleSpecifier}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (capabilities.executionProfile !== void 0 && !this.policy.allowedExecutionProfiles.includes(
|
|
340
|
+
capabilities.executionProfile
|
|
341
|
+
)) {
|
|
342
|
+
issues.push(
|
|
343
|
+
`Requested executionProfile ${capabilities.executionProfile} is not allowed`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (typeof capabilities.maxImports === "number" && capabilities.maxImports > this.policy.maxAllowedImports) {
|
|
347
|
+
issues.push(
|
|
348
|
+
`Requested maxImports ${capabilities.maxImports} exceeds policy limit ${this.policy.maxAllowedImports}`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (typeof capabilities.maxExecutionMs === "number" && capabilities.maxExecutionMs > this.policy.maxAllowedExecutionMs) {
|
|
352
|
+
issues.push(
|
|
353
|
+
`Requested maxExecutionMs ${capabilities.maxExecutionMs} exceeds policy limit ${this.policy.maxAllowedExecutionMs}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (typeof capabilities.maxComponentInvocations === "number" && capabilities.maxComponentInvocations > this.policy.maxAllowedComponentInvocations) {
|
|
357
|
+
issues.push(
|
|
358
|
+
`Requested maxComponentInvocations ${capabilities.maxComponentInvocations} exceeds policy limit ${this.policy.maxAllowedComponentInvocations}`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
for (const issue of issues) {
|
|
362
|
+
diagnostics.push({
|
|
363
|
+
level: "error",
|
|
364
|
+
code: "SECURITY_CAPABILITY_REJECTED",
|
|
365
|
+
message: issue
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
safe: issues.length === 0,
|
|
370
|
+
issues,
|
|
371
|
+
diagnostics
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
checkStateModel(state) {
|
|
375
|
+
const issues = [];
|
|
376
|
+
const transitions = state.transitions ?? {};
|
|
377
|
+
const transitionEntries = Object.entries(transitions);
|
|
378
|
+
if (transitionEntries.length > this.policy.maxTransitionsPerPlan) {
|
|
379
|
+
issues.push(
|
|
380
|
+
`Transition count ${transitionEntries.length} exceeds maximum ${this.policy.maxTransitionsPerPlan}`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
for (const [eventType, actions] of transitionEntries) {
|
|
384
|
+
if (actions.length > this.policy.maxActionsPerTransition) {
|
|
385
|
+
issues.push(
|
|
386
|
+
`Transition ${eventType} has ${actions.length} actions which exceeds maximum ${this.policy.maxActionsPerTransition}`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
for (const action of actions) {
|
|
390
|
+
issues.push(...this.checkAction(eventType, action));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return issues;
|
|
394
|
+
}
|
|
395
|
+
async checkRuntimeSource(source, moduleManifest) {
|
|
396
|
+
const issues = [];
|
|
397
|
+
if (!this.policy.allowRuntimeSourceModules) {
|
|
398
|
+
issues.push("Runtime source modules are disabled by policy");
|
|
399
|
+
return issues;
|
|
400
|
+
}
|
|
401
|
+
const sourceBytes = typeof TextEncoder !== "undefined" ? new TextEncoder().encode(source.code).length : source.code.length;
|
|
402
|
+
if (sourceBytes > this.policy.maxRuntimeSourceBytes) {
|
|
403
|
+
issues.push(
|
|
404
|
+
`Runtime source size ${sourceBytes} exceeds maximum ${this.policy.maxRuntimeSourceBytes} bytes`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const sourceImports = await this.parseSourceImports(source.code);
|
|
408
|
+
if (sourceImports.length > this.policy.maxSourceImportSpecifiers) {
|
|
409
|
+
issues.push(
|
|
410
|
+
`Runtime source import count ${sourceImports.length} exceeds maximum ${this.policy.maxSourceImportSpecifiers}`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
for (const sourceImport of sourceImports) {
|
|
414
|
+
const effectiveSpecifier = this.resolveManifestSpecifier(
|
|
415
|
+
sourceImport,
|
|
416
|
+
moduleManifest
|
|
417
|
+
);
|
|
418
|
+
const importCheck = this.checkModuleSpecifier(effectiveSpecifier);
|
|
419
|
+
issues.push(...importCheck.issues);
|
|
420
|
+
if (this.policy.requireModuleManifestForBareSpecifiers && this.isBareSpecifier(sourceImport) && !moduleManifest?.[sourceImport]) {
|
|
421
|
+
issues.push(
|
|
422
|
+
`Runtime source bare import requires manifest entry: ${sourceImport}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (!this.policy.allowDynamicSourceImports && /\bimport\s*\(/.test(source.code)) {
|
|
427
|
+
issues.push("Runtime source dynamic import() is disabled by policy");
|
|
428
|
+
}
|
|
429
|
+
for (const patternText of this.policy.sourceBannedPatternStrings) {
|
|
430
|
+
let pattern;
|
|
431
|
+
try {
|
|
432
|
+
pattern = new RegExp(patternText, "i");
|
|
433
|
+
} catch {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (pattern.test(source.code)) {
|
|
437
|
+
issues.push(`Runtime source contains blocked pattern: ${patternText}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return issues;
|
|
441
|
+
}
|
|
442
|
+
checkModuleManifest(moduleManifest) {
|
|
443
|
+
const issues = [];
|
|
444
|
+
for (const [specifier, descriptor] of Object.entries(moduleManifest)) {
|
|
445
|
+
if (specifier.trim().length === 0) {
|
|
446
|
+
issues.push("moduleManifest contains an empty specifier key");
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (descriptor.resolvedUrl.trim().length === 0) {
|
|
450
|
+
issues.push(`moduleManifest entry has empty resolvedUrl: ${specifier}`);
|
|
451
|
+
}
|
|
452
|
+
if (this.policy.requireModuleIntegrity && this.isUrl(descriptor.resolvedUrl) && (!descriptor.integrity || descriptor.integrity.trim().length === 0)) {
|
|
453
|
+
issues.push(
|
|
454
|
+
`moduleManifest entry requires integrity for remote module: ${specifier}`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const resolvedCheck = this.checkModuleSpecifier(descriptor.resolvedUrl);
|
|
458
|
+
issues.push(...resolvedCheck.issues);
|
|
459
|
+
}
|
|
460
|
+
return issues;
|
|
461
|
+
}
|
|
462
|
+
async checkManifestCoverage(plan, moduleManifest) {
|
|
463
|
+
const issues = [];
|
|
464
|
+
const requiredSpecifiers = /* @__PURE__ */ new Set();
|
|
465
|
+
const imports = plan.imports ?? [];
|
|
466
|
+
for (const specifier of imports) {
|
|
467
|
+
if (this.isBareSpecifier(specifier)) {
|
|
468
|
+
requiredSpecifiers.add(specifier);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
for (const specifier of plan.capabilities.allowedModules ?? []) {
|
|
472
|
+
if (this.isBareSpecifier(specifier)) {
|
|
473
|
+
requiredSpecifiers.add(specifier);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
walkNodes(plan.root, (node) => {
|
|
477
|
+
if (node.type === "component" && this.isBareSpecifier(node.module)) {
|
|
478
|
+
requiredSpecifiers.add(node.module);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
for (const sourceImport of await this.parseSourceImports(
|
|
482
|
+
plan.source?.code ?? ""
|
|
483
|
+
)) {
|
|
484
|
+
if (this.isBareSpecifier(sourceImport)) {
|
|
485
|
+
requiredSpecifiers.add(sourceImport);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
for (const specifier of requiredSpecifiers) {
|
|
489
|
+
if (!moduleManifest?.[specifier]) {
|
|
490
|
+
issues.push(
|
|
491
|
+
`Missing moduleManifest entry for bare specifier: ${specifier}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return issues;
|
|
496
|
+
}
|
|
497
|
+
resolveManifestSpecifier(specifier, moduleManifest) {
|
|
498
|
+
const descriptor = moduleManifest?.[specifier];
|
|
499
|
+
if (!descriptor || descriptor.resolvedUrl.trim().length === 0) {
|
|
500
|
+
return specifier;
|
|
501
|
+
}
|
|
502
|
+
return descriptor.resolvedUrl;
|
|
503
|
+
}
|
|
504
|
+
async parseSourceImports(code) {
|
|
505
|
+
if (code.trim().length === 0) {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
return (0, import_ir.collectRuntimeSourceImports)(code);
|
|
509
|
+
}
|
|
510
|
+
isBareSpecifier(specifier) {
|
|
511
|
+
return !specifier.startsWith("./") && !specifier.startsWith("../") && !specifier.startsWith("/") && !specifier.startsWith("http://") && !specifier.startsWith("https://") && !specifier.startsWith("data:") && !specifier.startsWith("blob:");
|
|
512
|
+
}
|
|
513
|
+
checkAction(eventType, action) {
|
|
514
|
+
const issues = [];
|
|
515
|
+
if (!(0, import_ir.isSafePath)(action.path)) {
|
|
516
|
+
issues.push(`Unsafe action path in ${eventType}: ${action.path}`);
|
|
517
|
+
}
|
|
518
|
+
if (action.type === "increment" && typeof action.by === "number" && !Number.isFinite(action.by)) {
|
|
519
|
+
issues.push(`Invalid increment value for ${eventType}: ${action.by}`);
|
|
520
|
+
}
|
|
521
|
+
if (action.type === "set" || action.type === "push") {
|
|
522
|
+
const value = action.value;
|
|
523
|
+
if ((0, import_ir.isRuntimeValueFromPath)(value)) {
|
|
524
|
+
const source = value.$from;
|
|
525
|
+
const allowedPrefix = source.startsWith("state.") || source.startsWith("event.") || source.startsWith("context.") || source.startsWith("vars.");
|
|
526
|
+
if (!allowedPrefix) {
|
|
527
|
+
issues.push(`Unsupported value source in ${eventType}: ${source}`);
|
|
528
|
+
}
|
|
529
|
+
if (!(0, import_ir.isSafePath)(source.replace(/^(state|event|context|vars)\./, ""))) {
|
|
530
|
+
issues.push(`Unsafe value source path in ${eventType}: ${source}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return issues;
|
|
535
|
+
}
|
|
536
|
+
isUrl(specifier) {
|
|
537
|
+
try {
|
|
538
|
+
const parsed = new URL(specifier);
|
|
539
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
540
|
+
} catch {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
function clonePolicy(policy) {
|
|
546
|
+
return {
|
|
547
|
+
...policy,
|
|
548
|
+
blockedTags: [...policy.blockedTags],
|
|
549
|
+
allowedModules: [...policy.allowedModules],
|
|
550
|
+
allowedNetworkHosts: [...policy.allowedNetworkHosts],
|
|
551
|
+
allowedExecutionProfiles: [...policy.allowedExecutionProfiles],
|
|
552
|
+
supportedSpecVersions: [...policy.supportedSpecVersions],
|
|
553
|
+
sourceBannedPatternStrings: [...policy.sourceBannedPatternStrings]
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function walkNodes(node, visitor) {
|
|
557
|
+
visitor(node);
|
|
558
|
+
if (node.type === "text") {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
for (const child of node.children ?? []) {
|
|
562
|
+
walkNodes(child, visitor);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function normalizeInitializationInput(input) {
|
|
566
|
+
if (!input) {
|
|
567
|
+
return {};
|
|
568
|
+
}
|
|
569
|
+
if (isSecurityInitializationOptions(input)) {
|
|
570
|
+
return input;
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
overrides: input
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function isSecurityInitializationOptions(value) {
|
|
577
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
return "profile" in value || "overrides" in value;
|
|
581
|
+
}
|
|
582
|
+
//# sourceMappingURL=security.cjs.js.map
|