dalila 1.9.21 → 1.9.24
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 +10 -5
- package/dist/cli/index.js +27 -5
- package/dist/cli/security-smoke.d.ts +8 -0
- package/dist/cli/security-smoke.js +502 -0
- package/dist/components/ui/runtime.d.ts +3 -1
- package/dist/components/ui/runtime.js +2 -0
- package/dist/components/ui/ui-types.d.ts +8 -8
- package/dist/core/html.d.ts +10 -3
- package/dist/core/html.js +10 -3
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/observability.d.ts +11 -0
- package/dist/core/observability.js +53 -0
- package/dist/core/persist.js +9 -1
- package/dist/core/signal.d.ts +34 -2
- package/dist/core/signal.js +102 -96
- package/dist/form/field-array.d.ts +12 -0
- package/dist/form/field-array.js +306 -0
- package/dist/form/form-dom.d.ts +6 -0
- package/dist/form/form-dom.js +52 -0
- package/dist/form/form-types.d.ts +24 -15
- package/dist/form/form.d.ts +7 -53
- package/dist/form/form.js +49 -932
- package/dist/form/index.d.ts +2 -2
- package/dist/form/index.js +1 -2
- package/dist/form/parse-form-data.d.ts +13 -0
- package/dist/form/parse-form-data.js +88 -0
- package/dist/form/path-utils.d.ts +25 -0
- package/dist/form/path-utils.js +127 -0
- package/dist/form/path-watchers.d.ts +23 -0
- package/dist/form/path-watchers.js +82 -0
- package/dist/form/validation-pipeline.d.ts +29 -0
- package/dist/form/validation-pipeline.js +223 -0
- package/dist/http/client.js +63 -16
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.js +1 -1
- package/dist/router/location-utils.d.ts +19 -0
- package/dist/router/location-utils.js +60 -0
- package/dist/router/lru-cache.d.ts +17 -0
- package/dist/router/lru-cache.js +63 -0
- package/dist/router/preload-metadata.d.ts +26 -0
- package/dist/router/preload-metadata.js +65 -0
- package/dist/router/router-lifecycle.d.ts +12 -0
- package/dist/router/router-lifecycle.js +31 -0
- package/dist/router/router-mount-lifecycle.d.ts +11 -0
- package/dist/router/router-mount-lifecycle.js +50 -0
- package/dist/router/router-prefetch.d.ts +22 -0
- package/dist/router/router-prefetch.js +86 -0
- package/dist/router/router-preload-cache.d.ts +25 -0
- package/dist/router/router-preload-cache.js +68 -0
- package/dist/router/router-render-utils.d.ts +14 -0
- package/dist/router/router-render-utils.js +11 -0
- package/dist/router/router-validation.d.ts +4 -0
- package/dist/router/router-validation.js +100 -0
- package/dist/router/router-view-composer.d.ts +16 -0
- package/dist/router/router-view-composer.js +41 -0
- package/dist/router/router.d.ts +12 -3
- package/dist/router/router.js +241 -621
- package/dist/runtime/array-directive-dom.d.ts +4 -0
- package/dist/runtime/array-directive-dom.js +30 -0
- package/dist/runtime/bind.d.ts +82 -1
- package/dist/runtime/bind.js +940 -935
- package/dist/runtime/boundary.d.ts +2 -2
- package/dist/runtime/boundary.js +19 -3
- package/dist/runtime/fromHtml.d.ts +10 -0
- package/dist/runtime/fromHtml.js +6 -4
- package/dist/runtime/html-sinks.d.ts +20 -0
- package/dist/runtime/html-sinks.js +165 -0
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/internal/components/component-props.d.ts +15 -0
- package/dist/runtime/internal/components/component-props.js +69 -0
- package/dist/runtime/internal/components/component-slots.d.ts +10 -0
- package/dist/runtime/internal/components/component-slots.js +68 -0
- package/dist/runtime/internal/list/list-clone-factory.d.ts +17 -0
- package/dist/runtime/internal/list/list-clone-factory.js +35 -0
- package/dist/runtime/internal/list/list-clone-registry.d.ts +12 -0
- package/dist/runtime/internal/list/list-clone-registry.js +43 -0
- package/dist/runtime/internal/list/list-keying.d.ts +13 -0
- package/dist/runtime/internal/list/list-keying.js +65 -0
- package/dist/runtime/internal/list/list-metadata.d.ts +11 -0
- package/dist/runtime/internal/list/list-metadata.js +21 -0
- package/dist/runtime/internal/list/list-reconcile.d.ts +3 -0
- package/dist/runtime/internal/list/list-reconcile.js +25 -0
- package/dist/runtime/internal/list/list-scheduler.d.ts +16 -0
- package/dist/runtime/internal/list/list-scheduler.js +45 -0
- package/dist/runtime/internal/virtual/virtual-list-helpers.d.ts +48 -0
- package/dist/runtime/internal/virtual/virtual-list-helpers.js +291 -0
- package/package.json +13 -6
- package/scripts/dev-server.cjs +445 -123
package/README.md
CHANGED
|
@@ -58,7 +58,9 @@ bind(document.getElementById('app')!, ctx);
|
|
|
58
58
|
### Core
|
|
59
59
|
|
|
60
60
|
- [Signals](./docs/core/signals.md)
|
|
61
|
+
- [Effects Guide](./docs/core/effects-guide.md)
|
|
61
62
|
- [Scopes](./docs/core/scope.md)
|
|
63
|
+
- [Scopes Guide](./docs/core/scopes-guide.md)
|
|
62
64
|
- [Persist](./docs/core/persist.md)
|
|
63
65
|
- [Context](./docs/context.md)
|
|
64
66
|
- [Scheduler](./docs/core/scheduler.md)
|
|
@@ -87,11 +89,13 @@ bind(document.getElementById('app')!, ctx);
|
|
|
87
89
|
|
|
88
90
|
- [Template Check CLI](./docs/cli/check.md)
|
|
89
91
|
- [Devtools Extension](./devtools-extension/README.md)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
92
|
+
- [Production Guide](./docs/production.md)
|
|
93
|
+
- [Security Hardening](./docs/security-hardening.md)
|
|
94
|
+
- [Threat Model](./docs/threat-model.md)
|
|
95
|
+
- [Recipes](./docs/recipes.md)
|
|
96
|
+
- [Upgrade Guide](./docs/upgrade.md)
|
|
97
|
+
- [Release Checklist](./docs/release-checklist.md)
|
|
98
|
+
- [Security Release Notes](./docs/security-release-notes.md)
|
|
95
99
|
|
|
96
100
|
## Packages
|
|
97
101
|
|
|
@@ -110,6 +114,7 @@ npm install
|
|
|
110
114
|
npm run build
|
|
111
115
|
npm run serve # Dev server with HMR
|
|
112
116
|
npm test
|
|
117
|
+
npm run test:watch
|
|
113
118
|
```
|
|
114
119
|
|
|
115
120
|
## License
|
package/dist/cli/index.js
CHANGED
|
@@ -16,7 +16,7 @@ Usage:
|
|
|
16
16
|
dalila routes init Initialize app and generate routes outputs
|
|
17
17
|
dalila routes watch [options] Watch routes and regenerate outputs on changes
|
|
18
18
|
dalila routes --help Show routes command help
|
|
19
|
-
dalila check [path] [--strict] Static analysis
|
|
19
|
+
dalila check [path] [--strict] Static analysis plus project security smoke
|
|
20
20
|
dalila help Show this help message
|
|
21
21
|
|
|
22
22
|
Options:
|
|
@@ -55,10 +55,11 @@ function showCheckHelp() {
|
|
|
55
55
|
🐰✂️ Dalila CLI - Check
|
|
56
56
|
|
|
57
57
|
Usage:
|
|
58
|
-
dalila check [path] [options] Static analysis of HTML templates
|
|
58
|
+
dalila check [path] [options] Static analysis of HTML templates + project security smoke
|
|
59
59
|
|
|
60
60
|
Validates that identifiers used in HTML templates ({expr}, d-* directives)
|
|
61
61
|
match the return type of the corresponding loader() in TypeScript.
|
|
62
|
+
Also runs project-wide security smoke checks for obvious raw HTML / XSS patterns.
|
|
62
63
|
|
|
63
64
|
Arguments:
|
|
64
65
|
[path] App directory to check (default: src/app)
|
|
@@ -160,6 +161,18 @@ function resolveDefaultAppDir(cwd) {
|
|
|
160
161
|
}
|
|
161
162
|
return path.join(appRoot, relToRoot);
|
|
162
163
|
}
|
|
164
|
+
function resolveDefaultSecuritySmokePath(cwd) {
|
|
165
|
+
const resolvedCwd = path.resolve(cwd);
|
|
166
|
+
const projectRoot = findProjectRoot(resolvedCwd);
|
|
167
|
+
if (!projectRoot) {
|
|
168
|
+
return resolveDefaultAppDir(cwd);
|
|
169
|
+
}
|
|
170
|
+
const srcRoot = path.join(projectRoot, 'src');
|
|
171
|
+
if (fs.existsSync(srcRoot) && fs.statSync(srcRoot).isDirectory()) {
|
|
172
|
+
return srcRoot;
|
|
173
|
+
}
|
|
174
|
+
return resolveDefaultAppDir(cwd);
|
|
175
|
+
}
|
|
163
176
|
function resolveGenerateConfig(cliArgs, cwd = process.cwd()) {
|
|
164
177
|
const dirIndex = cliArgs.indexOf('--dir');
|
|
165
178
|
const outputIndex = cliArgs.indexOf('--output');
|
|
@@ -368,9 +381,18 @@ async function main() {
|
|
|
368
381
|
const appDir = positional[0]
|
|
369
382
|
? path.resolve(positional[0])
|
|
370
383
|
: resolveDefaultAppDir(process.cwd());
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
384
|
+
const smokePath = positional[0]
|
|
385
|
+
? appDir
|
|
386
|
+
: resolveDefaultSecuritySmokePath(process.cwd());
|
|
387
|
+
const [{ runCheck }, { runSecuritySmokeChecks }] = await Promise.all([
|
|
388
|
+
import('./check.js'),
|
|
389
|
+
import('./security-smoke.js'),
|
|
390
|
+
]);
|
|
391
|
+
const [checkExitCode, smokeExitCode] = await Promise.all([
|
|
392
|
+
runCheck(appDir, { strict }),
|
|
393
|
+
runSecuritySmokeChecks(smokePath),
|
|
394
|
+
]);
|
|
395
|
+
process.exit(checkExitCode !== 0 || smokeExitCode !== 0 ? 1 : 0);
|
|
374
396
|
}
|
|
375
397
|
}
|
|
376
398
|
else if (command === '--help' || command === '-h') {
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
const INLINE_EVENT_HANDLER_MESSAGE = 'Inline event handler found in template. Use d-on-* instead.';
|
|
4
|
+
const JAVASCRIPT_URL_MESSAGE = 'Executable javascript: URL found in template sink.';
|
|
5
|
+
const EXECUTABLE_DATA_URL_MESSAGE = 'Executable data: URL found in template sink.';
|
|
6
|
+
const DANGEROUS_URL_PROTOCOL_MESSAGE = 'Dangerous URL protocol found in template sink.';
|
|
7
|
+
const SRCDOC_WARNING_MESSAGE = 'srcdoc embeds raw HTML. Review this iframe content as trusted-only.';
|
|
8
|
+
const D_HTML_WARNING_MESSAGE = 'd-html renders raw HTML. Keep the source trusted or sanitized.';
|
|
9
|
+
const D_ATTR_SRCDOC_WARNING_MESSAGE = 'd-attr-srcdoc writes raw iframe markup. Review the source as trusted-only.';
|
|
10
|
+
const HTML_INLINE_HANDLER_ATTR_NAMES = new Set([
|
|
11
|
+
'onabort',
|
|
12
|
+
'onafterprint',
|
|
13
|
+
'onanimationcancel',
|
|
14
|
+
'onanimationend',
|
|
15
|
+
'onanimationiteration',
|
|
16
|
+
'onanimationstart',
|
|
17
|
+
'onauxclick',
|
|
18
|
+
'onbeforeinput',
|
|
19
|
+
'onbeforematch',
|
|
20
|
+
'onbeforeprint',
|
|
21
|
+
'onbeforetoggle',
|
|
22
|
+
'onbeforeunload',
|
|
23
|
+
'onbegin',
|
|
24
|
+
'onblur',
|
|
25
|
+
'oncancel',
|
|
26
|
+
'oncanplay',
|
|
27
|
+
'oncanplaythrough',
|
|
28
|
+
'onchange',
|
|
29
|
+
'onclick',
|
|
30
|
+
'onclose',
|
|
31
|
+
'oncontextlost',
|
|
32
|
+
'oncontextmenu',
|
|
33
|
+
'oncontextrestored',
|
|
34
|
+
'oncopy',
|
|
35
|
+
'oncuechange',
|
|
36
|
+
'oncut',
|
|
37
|
+
'ondblclick',
|
|
38
|
+
'ondrag',
|
|
39
|
+
'ondragend',
|
|
40
|
+
'ondragenter',
|
|
41
|
+
'ondragleave',
|
|
42
|
+
'ondragover',
|
|
43
|
+
'ondragstart',
|
|
44
|
+
'ondrop',
|
|
45
|
+
'ondurationchange',
|
|
46
|
+
'onemptied',
|
|
47
|
+
'onend',
|
|
48
|
+
'onended',
|
|
49
|
+
'onerror',
|
|
50
|
+
'onfocus',
|
|
51
|
+
'onformdata',
|
|
52
|
+
'onfullscreenchange',
|
|
53
|
+
'onfullscreenerror',
|
|
54
|
+
'ongotpointercapture',
|
|
55
|
+
'onhashchange',
|
|
56
|
+
'oninput',
|
|
57
|
+
'oninvalid',
|
|
58
|
+
'onkeydown',
|
|
59
|
+
'onkeypress',
|
|
60
|
+
'onkeyup',
|
|
61
|
+
'onlanguagechange',
|
|
62
|
+
'onload',
|
|
63
|
+
'onloadeddata',
|
|
64
|
+
'onloadedmetadata',
|
|
65
|
+
'onloadstart',
|
|
66
|
+
'onlostpointercapture',
|
|
67
|
+
'onmessage',
|
|
68
|
+
'onmessageerror',
|
|
69
|
+
'onmousedown',
|
|
70
|
+
'onmouseenter',
|
|
71
|
+
'onmouseleave',
|
|
72
|
+
'onmousemove',
|
|
73
|
+
'onmouseout',
|
|
74
|
+
'onmouseover',
|
|
75
|
+
'onmouseup',
|
|
76
|
+
'onoffline',
|
|
77
|
+
'ononline',
|
|
78
|
+
'onpagehide',
|
|
79
|
+
'onpageshow',
|
|
80
|
+
'onpaste',
|
|
81
|
+
'onpause',
|
|
82
|
+
'onplay',
|
|
83
|
+
'onplaying',
|
|
84
|
+
'onpointercancel',
|
|
85
|
+
'onpointerdown',
|
|
86
|
+
'onpointerenter',
|
|
87
|
+
'onpointerleave',
|
|
88
|
+
'onpointermove',
|
|
89
|
+
'onpointerout',
|
|
90
|
+
'onpointerover',
|
|
91
|
+
'onpointerrawupdate',
|
|
92
|
+
'onpointerup',
|
|
93
|
+
'onpopstate',
|
|
94
|
+
'onprogress',
|
|
95
|
+
'onratechange',
|
|
96
|
+
'onrepeat',
|
|
97
|
+
'onreset',
|
|
98
|
+
'onresize',
|
|
99
|
+
'onscroll',
|
|
100
|
+
'onscrollend',
|
|
101
|
+
'onsecuritypolicyviolation',
|
|
102
|
+
'onseeked',
|
|
103
|
+
'onseeking',
|
|
104
|
+
'onselect',
|
|
105
|
+
'onselectionchange',
|
|
106
|
+
'onselectstart',
|
|
107
|
+
'onslotchange',
|
|
108
|
+
'onstalled',
|
|
109
|
+
'onstorage',
|
|
110
|
+
'onsubmit',
|
|
111
|
+
'onsuspend',
|
|
112
|
+
'ontimeupdate',
|
|
113
|
+
'ontoggle',
|
|
114
|
+
'ontransitioncancel',
|
|
115
|
+
'ontransitionend',
|
|
116
|
+
'ontransitionrun',
|
|
117
|
+
'ontransitionstart',
|
|
118
|
+
'onunhandledrejection',
|
|
119
|
+
'onunload',
|
|
120
|
+
'onvolumechange',
|
|
121
|
+
'onwaiting',
|
|
122
|
+
'onwebkitanimationend',
|
|
123
|
+
'onwebkitanimationiteration',
|
|
124
|
+
'onwebkitanimationstart',
|
|
125
|
+
'onwebkittransitionend',
|
|
126
|
+
'onwheel',
|
|
127
|
+
]);
|
|
128
|
+
const HTML_URL_ATTR_NAMES = new Set([
|
|
129
|
+
'href',
|
|
130
|
+
'src',
|
|
131
|
+
'xlink:href',
|
|
132
|
+
'formaction',
|
|
133
|
+
'action',
|
|
134
|
+
'poster',
|
|
135
|
+
'data',
|
|
136
|
+
]);
|
|
137
|
+
const SAFE_URL_PROTOCOLS = new Set([
|
|
138
|
+
'http:',
|
|
139
|
+
'https:',
|
|
140
|
+
'mailto:',
|
|
141
|
+
'tel:',
|
|
142
|
+
'sms:',
|
|
143
|
+
'blob:',
|
|
144
|
+
]);
|
|
145
|
+
const EXECUTABLE_DATA_URL_PATTERN = /^data:(?:text\/html|application\/xhtml\+xml|image\/svg\+xml)\b/i;
|
|
146
|
+
const SOURCE_SECURITY_PATTERNS = [
|
|
147
|
+
{
|
|
148
|
+
severity: 'warning',
|
|
149
|
+
regex: /\b(?:innerHTML|outerHTML)\s*=/g,
|
|
150
|
+
message: 'Raw HTML sink assignment found in source. Review the input as trusted-only.',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
severity: 'warning',
|
|
154
|
+
regex: /\binsertAdjacentHTML\s*\(/g,
|
|
155
|
+
message: 'insertAdjacentHTML() found in source. Review the inserted markup as trusted-only.',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
severity: 'warning',
|
|
159
|
+
regex: /\bdocument\.write\s*\(/g,
|
|
160
|
+
message: 'document.write() found in source. Review the written markup as trusted-only.',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
severity: 'warning',
|
|
164
|
+
regex: /\bfromHtml\s*\(/g,
|
|
165
|
+
message: 'fromHtml() found in source. Pass only trusted template markup.',
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
function findProjectRoot(startDir) {
|
|
169
|
+
let current = path.resolve(startDir);
|
|
170
|
+
while (true) {
|
|
171
|
+
if (fs.existsSync(path.join(current, 'package.json'))
|
|
172
|
+
|| fs.existsSync(path.join(current, 'tsconfig.json'))) {
|
|
173
|
+
return current;
|
|
174
|
+
}
|
|
175
|
+
const parent = path.dirname(current);
|
|
176
|
+
if (parent === current) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
current = parent;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function findExistingAncestor(startPath) {
|
|
183
|
+
let current = path.resolve(startPath);
|
|
184
|
+
while (!fs.existsSync(current)) {
|
|
185
|
+
const parent = path.dirname(current);
|
|
186
|
+
if (parent === current) {
|
|
187
|
+
return current;
|
|
188
|
+
}
|
|
189
|
+
current = parent;
|
|
190
|
+
}
|
|
191
|
+
return current;
|
|
192
|
+
}
|
|
193
|
+
function resolveSecuritySmokeScope(scanPath) {
|
|
194
|
+
const resolvedPath = path.resolve(scanPath);
|
|
195
|
+
const existingPath = findExistingAncestor(resolvedPath);
|
|
196
|
+
const startDir = fs.statSync(existingPath).isDirectory()
|
|
197
|
+
? existingPath
|
|
198
|
+
: path.dirname(existingPath);
|
|
199
|
+
const projectRoot = findProjectRoot(startDir) ?? startDir;
|
|
200
|
+
return {
|
|
201
|
+
projectRoot,
|
|
202
|
+
scanRoot: resolvedPath,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function collectCandidateFiles(dir, files = []) {
|
|
206
|
+
if (!fs.existsSync(dir))
|
|
207
|
+
return files;
|
|
208
|
+
const stat = fs.statSync(dir);
|
|
209
|
+
if (stat.isFile()) {
|
|
210
|
+
const ext = path.extname(dir).toLowerCase();
|
|
211
|
+
if (ext === '.html' || ext === '.ts' || ext === '.js') {
|
|
212
|
+
files.push(dir);
|
|
213
|
+
}
|
|
214
|
+
return files;
|
|
215
|
+
}
|
|
216
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
217
|
+
if (entry.name.startsWith('.'))
|
|
218
|
+
continue;
|
|
219
|
+
if (entry.name === 'dist' || entry.name === 'node_modules')
|
|
220
|
+
continue;
|
|
221
|
+
const fullPath = path.join(dir, entry.name);
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
collectCandidateFiles(fullPath, files);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!entry.isFile())
|
|
227
|
+
continue;
|
|
228
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
229
|
+
if (ext === '.html' || ext === '.ts' || ext === '.js') {
|
|
230
|
+
files.push(fullPath);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return files;
|
|
234
|
+
}
|
|
235
|
+
function getLineCol(source, offset) {
|
|
236
|
+
const before = source.slice(0, offset);
|
|
237
|
+
const lines = before.split('\n');
|
|
238
|
+
return {
|
|
239
|
+
line: lines.length,
|
|
240
|
+
col: lines[lines.length - 1].length + 1,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function isHtmlTagStartChar(char) {
|
|
244
|
+
return !!char && /[a-zA-Z]/.test(char);
|
|
245
|
+
}
|
|
246
|
+
function collectHtmlStartTags(source) {
|
|
247
|
+
const tags = [];
|
|
248
|
+
let i = 0;
|
|
249
|
+
while (i < source.length) {
|
|
250
|
+
if (source[i] !== '<') {
|
|
251
|
+
i += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const next = source[i + 1];
|
|
255
|
+
if (next === '/' || next === '!' || next === '?' || !isHtmlTagStartChar(next)) {
|
|
256
|
+
i += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
i += 1;
|
|
260
|
+
const tagNameStart = i;
|
|
261
|
+
while (i < source.length && /[\w:-]/.test(source[i])) {
|
|
262
|
+
i += 1;
|
|
263
|
+
}
|
|
264
|
+
const tagName = source.slice(tagNameStart, i);
|
|
265
|
+
if (!tagName) {
|
|
266
|
+
i += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const attrs = [];
|
|
270
|
+
let closed = false;
|
|
271
|
+
while (i < source.length) {
|
|
272
|
+
while (i < source.length && /\s/.test(source[i])) {
|
|
273
|
+
i += 1;
|
|
274
|
+
}
|
|
275
|
+
if (i >= source.length)
|
|
276
|
+
break;
|
|
277
|
+
if (source[i] === '>') {
|
|
278
|
+
i += 1;
|
|
279
|
+
closed = true;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
if (source[i] === '/' && source[i + 1] === '>') {
|
|
283
|
+
i += 2;
|
|
284
|
+
closed = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
if (source[i] === '/') {
|
|
288
|
+
i += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const attrStart = i;
|
|
292
|
+
while (i < source.length && !/[\s=/>]/.test(source[i])) {
|
|
293
|
+
i += 1;
|
|
294
|
+
}
|
|
295
|
+
if (i === attrStart) {
|
|
296
|
+
i += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const name = source.slice(attrStart, i);
|
|
300
|
+
while (i < source.length && /\s/.test(source[i])) {
|
|
301
|
+
i += 1;
|
|
302
|
+
}
|
|
303
|
+
let value = null;
|
|
304
|
+
if (source[i] === '=') {
|
|
305
|
+
i += 1;
|
|
306
|
+
while (i < source.length && /\s/.test(source[i])) {
|
|
307
|
+
i += 1;
|
|
308
|
+
}
|
|
309
|
+
if (i >= source.length)
|
|
310
|
+
break;
|
|
311
|
+
const quote = source[i];
|
|
312
|
+
if (quote === '"' || quote === "'") {
|
|
313
|
+
i += 1;
|
|
314
|
+
const valueStart = i;
|
|
315
|
+
while (i < source.length && source[i] !== quote) {
|
|
316
|
+
i += 1;
|
|
317
|
+
}
|
|
318
|
+
value = source.slice(valueStart, i);
|
|
319
|
+
if (i < source.length) {
|
|
320
|
+
i += 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const valueStart = i;
|
|
325
|
+
while (i < source.length && !/[\s>]/.test(source[i])) {
|
|
326
|
+
i += 1;
|
|
327
|
+
}
|
|
328
|
+
value = source.slice(valueStart, i);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
attrs.push({ name, value, index: attrStart });
|
|
332
|
+
}
|
|
333
|
+
if (closed) {
|
|
334
|
+
tags.push({ tagName, attrs });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return tags;
|
|
338
|
+
}
|
|
339
|
+
function normalizeProtocolCheckValue(value) {
|
|
340
|
+
return value.replace(/[\u0000-\u0020\u007f]+/g, '').toLowerCase();
|
|
341
|
+
}
|
|
342
|
+
function extractUrlProtocol(value) {
|
|
343
|
+
const match = value.match(/^([a-z][a-z0-9+\-.]*):/i);
|
|
344
|
+
return match ? `${match[1].toLowerCase()}:` : null;
|
|
345
|
+
}
|
|
346
|
+
function classifyDangerousHtmlUrl(tagName, attrName, value) {
|
|
347
|
+
const normalizedAttrName = attrName.toLowerCase();
|
|
348
|
+
if (!HTML_URL_ATTR_NAMES.has(normalizedAttrName))
|
|
349
|
+
return null;
|
|
350
|
+
if (normalizedAttrName === 'data' && tagName.toLowerCase() !== 'object') {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
if (value == null)
|
|
354
|
+
return null;
|
|
355
|
+
const normalized = normalizeProtocolCheckValue(value);
|
|
356
|
+
if (!normalized
|
|
357
|
+
|| normalized.startsWith('/')
|
|
358
|
+
|| normalized.startsWith('./')
|
|
359
|
+
|| normalized.startsWith('../')
|
|
360
|
+
|| normalized.startsWith('?')
|
|
361
|
+
|| normalized.startsWith('#')) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const protocol = extractUrlProtocol(normalized);
|
|
365
|
+
if (!protocol)
|
|
366
|
+
return null;
|
|
367
|
+
if (protocol === 'javascript:')
|
|
368
|
+
return JAVASCRIPT_URL_MESSAGE;
|
|
369
|
+
if (EXECUTABLE_DATA_URL_PATTERN.test(normalized))
|
|
370
|
+
return EXECUTABLE_DATA_URL_MESSAGE;
|
|
371
|
+
if (!SAFE_URL_PROTOCOLS.has(protocol))
|
|
372
|
+
return DANGEROUS_URL_PROTOCOL_MESSAGE;
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
function collectHtmlTemplateFindings(source, filePath, findings) {
|
|
376
|
+
for (const tag of collectHtmlStartTags(source)) {
|
|
377
|
+
for (const attr of tag.attrs) {
|
|
378
|
+
const attrName = attr.name.toLowerCase();
|
|
379
|
+
const { line, col } = getLineCol(source, attr.index);
|
|
380
|
+
if (HTML_INLINE_HANDLER_ATTR_NAMES.has(attrName)) {
|
|
381
|
+
findings.push({
|
|
382
|
+
filePath,
|
|
383
|
+
line,
|
|
384
|
+
col,
|
|
385
|
+
severity: 'error',
|
|
386
|
+
message: INLINE_EVENT_HANDLER_MESSAGE,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const dangerousUrlMessage = classifyDangerousHtmlUrl(tag.tagName, attrName, attr.value);
|
|
390
|
+
if (dangerousUrlMessage) {
|
|
391
|
+
findings.push({
|
|
392
|
+
filePath,
|
|
393
|
+
line,
|
|
394
|
+
col,
|
|
395
|
+
severity: 'error',
|
|
396
|
+
message: dangerousUrlMessage,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (attrName === 'srcdoc') {
|
|
400
|
+
findings.push({
|
|
401
|
+
filePath,
|
|
402
|
+
line,
|
|
403
|
+
col,
|
|
404
|
+
severity: 'warning',
|
|
405
|
+
message: SRCDOC_WARNING_MESSAGE,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (attrName === 'd-html') {
|
|
409
|
+
findings.push({
|
|
410
|
+
filePath,
|
|
411
|
+
line,
|
|
412
|
+
col,
|
|
413
|
+
severity: 'warning',
|
|
414
|
+
message: D_HTML_WARNING_MESSAGE,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (attrName === 'd-attr-srcdoc') {
|
|
418
|
+
findings.push({
|
|
419
|
+
filePath,
|
|
420
|
+
line,
|
|
421
|
+
col,
|
|
422
|
+
severity: 'warning',
|
|
423
|
+
message: D_ATTR_SRCDOC_WARNING_MESSAGE,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function collectPatternFindings(source, filePath, patterns, findings) {
|
|
430
|
+
for (const pattern of patterns) {
|
|
431
|
+
pattern.regex.lastIndex = 0;
|
|
432
|
+
let match = null;
|
|
433
|
+
while ((match = pattern.regex.exec(source)) !== null) {
|
|
434
|
+
const { line, col } = getLineCol(source, match.index);
|
|
435
|
+
findings.push({
|
|
436
|
+
filePath,
|
|
437
|
+
line,
|
|
438
|
+
col,
|
|
439
|
+
severity: pattern.severity,
|
|
440
|
+
message: pattern.message,
|
|
441
|
+
});
|
|
442
|
+
if (match[0].length === 0) {
|
|
443
|
+
pattern.regex.lastIndex += 1;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
export async function runSecuritySmokeChecks(scanPath) {
|
|
449
|
+
const { projectRoot, scanRoot } = resolveSecuritySmokeScope(scanPath);
|
|
450
|
+
if (!fs.existsSync(scanRoot)) {
|
|
451
|
+
return 1;
|
|
452
|
+
}
|
|
453
|
+
const findings = [];
|
|
454
|
+
const files = collectCandidateFiles(scanRoot);
|
|
455
|
+
for (const filePath of files) {
|
|
456
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
457
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
458
|
+
if (ext === '.html') {
|
|
459
|
+
collectHtmlTemplateFindings(source, filePath, findings);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
collectPatternFindings(source, filePath, SOURCE_SECURITY_PATTERNS, findings);
|
|
463
|
+
}
|
|
464
|
+
const errors = findings.filter((finding) => finding.severity === 'error');
|
|
465
|
+
const warnings = findings.filter((finding) => finding.severity === 'warning');
|
|
466
|
+
console.log('🛡 Dalila Security Smoke');
|
|
467
|
+
if (findings.length === 0) {
|
|
468
|
+
console.log('✅ No dangerous template patterns found');
|
|
469
|
+
console.log('');
|
|
470
|
+
return 0;
|
|
471
|
+
}
|
|
472
|
+
const grouped = new Map();
|
|
473
|
+
for (const finding of findings) {
|
|
474
|
+
const bucket = grouped.get(finding.filePath) ?? [];
|
|
475
|
+
bucket.push(finding);
|
|
476
|
+
grouped.set(finding.filePath, bucket);
|
|
477
|
+
}
|
|
478
|
+
for (const [filePath, bucket] of grouped) {
|
|
479
|
+
console.log(` ${path.relative(projectRoot, filePath)}`);
|
|
480
|
+
for (const finding of bucket) {
|
|
481
|
+
const prefix = finding.severity === 'error' ? '❌' : '⚠️';
|
|
482
|
+
console.log(` ${`${finding.line}:${finding.col}`.padEnd(8)} ${prefix} ${finding.message}`);
|
|
483
|
+
}
|
|
484
|
+
console.log('');
|
|
485
|
+
}
|
|
486
|
+
const summaryParts = [];
|
|
487
|
+
if (errors.length > 0) {
|
|
488
|
+
summaryParts.push(`${errors.length} error${errors.length === 1 ? '' : 's'}`);
|
|
489
|
+
}
|
|
490
|
+
if (warnings.length > 0) {
|
|
491
|
+
summaryParts.push(`${warnings.length} warning${warnings.length === 1 ? '' : 's'}`);
|
|
492
|
+
}
|
|
493
|
+
const summary = summaryParts.join(' and ');
|
|
494
|
+
if (errors.length > 0) {
|
|
495
|
+
console.log(`❌ Security smoke found ${summary}`);
|
|
496
|
+
console.log('');
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
console.log(`⚠️ Security smoke found ${summary}`);
|
|
500
|
+
console.log('');
|
|
501
|
+
return 0;
|
|
502
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type Signal } from "../../core/signal.js";
|
|
2
|
-
import { type BindContext } from "../../runtime/bind.js";
|
|
2
|
+
import { type BindContext, type BindOptions } from "../../runtime/bind.js";
|
|
3
3
|
import type { Calendar, Combobox, Dialog, Drawer, Dropdown, Dropzone, PopoverMount, TabsMount, Toast } from "./ui-types.js";
|
|
4
4
|
export interface MountUIOptions {
|
|
5
5
|
context?: BindContext;
|
|
6
6
|
events?: string[];
|
|
7
7
|
theme?: boolean;
|
|
8
8
|
sliderValue?: Signal<string>;
|
|
9
|
+
sanitizeHtml?: BindOptions["sanitizeHtml"];
|
|
10
|
+
security?: BindOptions["security"];
|
|
9
11
|
dialogs?: Record<string, Dialog>;
|
|
10
12
|
drawers?: Record<string, Drawer>;
|
|
11
13
|
dropdowns?: Record<string, Dropdown>;
|
|
@@ -354,6 +354,8 @@ export function mountUI(root, options) {
|
|
|
354
354
|
withScope(scope, () => {
|
|
355
355
|
cleanups.push(bind(mountedRoot, ctx, {
|
|
356
356
|
events: options.events ?? DEFAULT_EVENTS,
|
|
357
|
+
sanitizeHtml: options.sanitizeHtml,
|
|
358
|
+
security: options.security,
|
|
357
359
|
}));
|
|
358
360
|
// Attach dialogs
|
|
359
361
|
for (const [key, dialog] of Object.entries(options.dialogs ?? {})) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Signal } from "../../core/signal.js";
|
|
1
|
+
import type { ComputedSignal, ReadonlySignal, Signal } from "../../core/signal.js";
|
|
2
2
|
export interface DialogOptions {
|
|
3
3
|
closeOnBackdrop?: boolean;
|
|
4
4
|
closeOnEscape?: boolean;
|
|
@@ -56,9 +56,9 @@ export interface Tabs {
|
|
|
56
56
|
_attachTo(el: HTMLElement): void;
|
|
57
57
|
}
|
|
58
58
|
export interface TabBindings {
|
|
59
|
-
tabClass:
|
|
60
|
-
selected:
|
|
61
|
-
visible:
|
|
59
|
+
tabClass: ComputedSignal<string>;
|
|
60
|
+
selected: ComputedSignal<string>;
|
|
61
|
+
visible: ComputedSignal<boolean>;
|
|
62
62
|
}
|
|
63
63
|
export interface DropdownOptions {
|
|
64
64
|
closeOnSelect?: boolean;
|
|
@@ -84,7 +84,7 @@ export interface Combobox {
|
|
|
84
84
|
query: Signal<string>;
|
|
85
85
|
value: Signal<string>;
|
|
86
86
|
label: Signal<string>;
|
|
87
|
-
filtered:
|
|
87
|
+
filtered: ReadonlySignal<ComboboxOption[]>;
|
|
88
88
|
highlightedIndex: Signal<number>;
|
|
89
89
|
show(): void;
|
|
90
90
|
close(): void;
|
|
@@ -103,7 +103,7 @@ export interface Accordion {
|
|
|
103
103
|
toggle(itemId: string): void;
|
|
104
104
|
open(itemId: string): void;
|
|
105
105
|
close(itemId: string): void;
|
|
106
|
-
isOpen(itemId: string):
|
|
106
|
+
isOpen(itemId: string): ReadonlySignal<boolean>;
|
|
107
107
|
_attachTo(el: HTMLElement): void;
|
|
108
108
|
}
|
|
109
109
|
export interface CalendarDay {
|
|
@@ -125,8 +125,8 @@ export interface Calendar {
|
|
|
125
125
|
year: Signal<number>;
|
|
126
126
|
month: Signal<number>;
|
|
127
127
|
selected: Signal<Date | null>;
|
|
128
|
-
title:
|
|
129
|
-
days:
|
|
128
|
+
title: ReadonlySignal<string>;
|
|
129
|
+
days: ReadonlySignal<CalendarDay[]>;
|
|
130
130
|
dayLabels: string[];
|
|
131
131
|
prev(): void;
|
|
132
132
|
next(): void;
|