Conversation
#182) ## Summary Fixes #162 (Critical). The anti-mXSS double-parse in `sanitizeHtmlCore` returned **raw, unescaped** `fragment.textContent` when the serialize→re-parse output was unstable. Every caller assigns the return value to an HTML sink (`innerHTML`/`insertAdjacentHTML` via `.html()`, `.append()`, `bq-html`, …), so an attacker could smuggle an entity-encoded payload (`<img src=x onerror=...>`) plus a construct that forces re-parse instability (`<a><table><a>…` foster-parenting) and get live markup back — the mXSS defense itself was the injection sink. ## Fix The text fallback is now HTML-escaped before being returned (local `escapeHtmlText` helper in `sanitize-core.ts` to avoid a circular import with `sanitize.ts`). ## Verification - New regression test with the exact audit payload `<a><table><a><img src=x onerror=alert(1)>`; asserts no `<img>` element materializes when the result is assigned to `innerHTML`. - Verified the fallback branch actually fires under happy-dom: output is now `<img src=x onerror=alert(1)>` (inert text). - `bun test tests/security.test.ts`: 59 pass / 0 fail. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #165 (High). `notifySubscribers` iterated the live `subscribers` array while unsubscribe `splice`s it, so a self-detaching watcher caused the next subscriber to be silently skipped for that notification cycle. ## Fix Iterate a snapshot (`[...subscribers]`), mirroring the existing `$onAction` listener-snapshot guard in the same file. ## Verification - Regression test with the exact issue repro (subscriber A self-unsubscribes, subscriber B must still fire); fails without the fix, passes with it. - `bun test tests/store.test.ts`: 95 pass / 0 fail. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #167 (Medium; High where templates can carry untrusted content). The DOM-backed `evaluateSSR` (active whenever a global/configured `DOMParser` exists) had two problems: 1. It fell back to `new Function(...keys, `return (${expr});`)` for any non-trivial expression — `unsafe-eval` on a path whose sibling `expression.ts` was written specifically to avoid it. 2. Its dot-notation fast-lane didn't call `isPrototypePollutionKey`, so `constructor.constructor` walked the prototype chain and returned the `Function` constructor. ## Fix `evaluateSSR` now delegates to the shared, CSP-safe `evaluateExpression` (Pratt parser in `expression.ts`) already used by the pure renderer. This removes the `new Function` fallback and the proto-lookup gap in one step, and unifies evaluator behaviour across both backends. The dead `unwrap`/`isSignal`/`isComputed` imports are dropped (unwrapping lives in the shared evaluator). ## Verification - New test: `constructor.constructor` on the DOM backend yields no `Function` (fails on the old code, passes now). - New test: complex arithmetic (`a + b`) still evaluates via the safe parser. - Grep confirms no `new Function` remains in `src/ssr`. - Full suite: 2988 pass / 0 fail. `tsc --noEmit` + eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
…literals (#190) ## Summary Fixes #170 (Medium; build-breaking). `scanStringLiteral` returned `i + 1` unconditionally and never verified the closing quote, so an unterminated literal (`<p bq-text="'oops">`) was copied verbatim and `compileExpression` returned `{ ok: true }` with a broken body. Because `emitModule` writes every expression into **one** module, a single malformed entry throws at import time and takes down *all* precompiled expressions in that file — with no per-expression runtime fallback. Malformed numeric literals (`1ex`) hit the same class of bug via `scanNumericLiteral`. ## Fix - `scanStringLiteral` throws `Bail('unterminated string literal')` when it reaches end-of-input without the matching quote. - Scanned numeric runs are validated against a numeric-literal regex (decimal/hex/octal/binary with `_` separators); invalid runs throw `Bail('invalid numeric literal')`. Both bails route through the existing `try/catch` in `compileExpression` to `{ ok: false, reason }`, so the expression cleanly falls back to the runtime evaluator instead of poisoning the module. ## Verification - New tests: unterminated `'oops` / `"unclosed` / `greeting + 'tail` bail; escaped-quote strings still compile; `1ex`/`1e`/`0xG1` bail while `1`, `1.5`, `0xFF`, `1e3`, `1_000`, `.5`, `0b1010` still compile. Both new tests fail on the pre-fix code. - View suites (compiler/view/stable/1-14): all pass. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #171 (Medium). `createTrustedHtml` / `getTrustedTypesPolicy` existed but **no DOM-write path called them** — they were only re-exported. So under an enforced `require-trusted-types-for 'script'` CSP, every `innerHTML =`/`insertAdjacentHTML` in `dom.ts`/`collection.ts`/`element.ts`/directives threw (framework fails closed but non-functional), and the documented "Trusted Types / CSP compliance" property was simply absent. ## Fix New `trustedHtmlForSink(rawHtml)` in `security/trusted-types.ts`: returns a `TrustedHTML` when a policy is active (so the write satisfies enforced TT), otherwise the sanitized string — sanitizing exactly once. All **sanitized** sinks now route through it: - `core/dom.ts`: `setHtml`, `createElementFromHtml`, `insertContent` - `core/collection.ts`: `Collection.html()`, `insertAll()` - `view/directives`: `bq-html` (sanitized branch), `bq-html-safe` - `element.ts` `html()` inherits it via `setHtml` The explicit `htmlUnsafe()` / `sanitize: false` escape hatches are deliberately left as raw writes (bypassing sanitization is their documented purpose). The `sanitizeHtml` JSDoc is corrected to describe actual behavior (returns a branded string; the framework's sinks wrap it for TT). ## Verification - New test proves the **positive path**: with a mock TT policy installed (fresh module instance to avoid cached-policy leakage), `trustedHtmlForSink` invokes `policy.createHTML` and returns the branded `TrustedHTML`. - New test for the **fallback path**: returns a sanitized, sink-safe string in a non-TT environment. - Existing sink sanitization tests still pass (behavior unchanged without TT). - Full suite: 2988 pass / 0 fail. `tsc --noEmit` + eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
#192) ## Summary Fixes #172 (Medium; correctness / resource exhaustion). `useFetch` kept a single shared `currentAbortController` and overwrote it on every `execute()`. When executions overlap (a `watch` refresh racing a manual `refresh()`/`execute()`), the earlier in-flight fetch was **never aborted** — its result was discarded by the `executionId` guard, but the request kept consuming network/CPU, and `state.abort()` could only cancel the most recent controller. Rapid `watch` churn piled up un-cancellable requests. ## Fix `execute()` aborts the previous controller (`currentAbortController?.abort()`) before creating the new one. The superseded request is cancelled immediately. The existing `finally` cleanup already only nulls the controller when it's still the current one, so no cross-execution clobbering. ## Verification - New test: two overlapping executions; the first request's `AbortSignal` becomes `aborted`, the second stays live. Fails without the fix. - Reactive/http/network/concurrency suites: 335 pass / 0 fail. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #173 (Medium; memory leaks). Three composables created long-lived reactive primitives with no disposal path, leaking whenever created outside an active `effectScope`. ## Fixes - **`deferred()`** — created an internal `effect` (and its scheduled timer/idle callback) but returned only `readonly(mirror)`, no disposer. Now returns `ReadonlySignalHandle<T> & { dispose() }`; `dispose()` stops the effect (its cleanup cancels any pending timer) and disposes the mirror. Scope-managed behavior when inside a scope is unchanged. - **`persistedSignal()`** — the persistence `effect` auto-registered with the ambient scope, so `scope.stop()` silently stopped persistence while the returned signal kept living, and there was no other way to stop it. Persistence now runs in a **detached** scope tied to the signal's own `dispose()`: an ambient `scope.stop()` no longer affects it, and `signal.dispose()` stops persistence. - **`effectScope(detached?)`** — new optional param (mirrors Vue's `effectScope(true)`): when `true` the scope isn't auto-collected by an enclosing scope. Additive, backward-compatible. (The third sub-point in the issue — module-scope `computed()` retention — is a documented must-dispose contract; `computed` already exposes `dispose()`. This PR addresses the two composables that had *no* disposal path at all.) ## Verification - `deferred`: new test asserts source changes stop flowing after `dispose()`. - `persistedSignal`: new tests assert persistence survives an ambient `scope.stop()` and stops after `signal.dispose()`. Both new persist tests + the deferred test fail on the pre-fix code. - Full suite: 2989 pass / 0 fail. `tsc --noEmit` + eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
…#194) ## Summary Fixes #174 (Low; contract violation + minor info-shape leak). `interpolate` and the ICU `arg`/`select`/`plural` sites used the `in` operator (and `resolveKey` walked key segments without a guard), which resolves inherited `Object.prototype` members. A placeholder or key matching `toString`/`valueOf`/`constructor`/`__proto__`/… was treated as present even when `params`/`messages` didn't own it, substituting the inherited value (e.g. `Hello {toString}` → the native function source) instead of leaving the literal `{name}` intact. ## Fix All lookup sites use `Object.prototype.hasOwnProperty.call(...)`: - `translate.ts`: `interpolate` placeholder check + defensive guard in `resolveKey` - `icu.ts`: shared `hasParam()` helper used by the `arg`, `select`, and `plural` cases ## Verification - New tests: `{toString}`/`{constructor}` placeholders left intact; real params still substitute; `resolveKey` returns `undefined` for inherited names; ICU `arg`/`select` unaffected by inherited names. All three fail on the pre-fix code. - i18n suites: 110 pass / 0 fail. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #175 (Low; robustness). The store's `deepClone` assigned via bracket notation over `Object.keys`, so a state object with an own enumerable `__proto__` key (e.g. from `JSON.parse('{"__proto__":{…}}')`) triggered the `__proto__` setter and reassigned the **clone's** prototype instead of copying a data property, corrupting the object `$patchDeep` operates on. Contained (no global `Object.prototype` pollution) but a fidelity bug. ## Fix `deepClone` skips prototype-pollution keys via the shared `isPrototypePollutionKey` helper from `core/utils/object.ts` (already used by `merge`). ## Verification - New test: cloning a `JSON.parse`'d `{"__proto__":{polluted:true},"safe":1}` yields a clone whose prototype is still `Object.prototype`, preserves `safe`, and does not pollute `{}`. Fails on the pre-fix code. - Store suite: 95 pass / 0 fail. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #177 (Low; correctness — not exploitable). `titleTemplate` was applied with `state.titleTemplate.replace(/%s/g, state.title)`. Because the title is the **replacement string**, special replacement patterns (`$&`, `$1`, `` $` ``, `$'`) in the title were interpreted by `String.prototype.replace` and mangled the output — e.g. a page titled `Q&A $& more` rendered incorrectly. ## Fix Use a replacer **function** (`() => state.title`) so the title is inserted literally. Output is still `escapeText`'d as before. ## Verification - New test: a title containing `$&` renders literally under a `%s | Acme` template. Fails on the pre-fix code. - ssr-runtime suite: 107 pass / 0 fail. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #179 (Low; defense-in-depth). DOM-clobbering protection only stripped `id`/`name` values whose exact value was in a ~30-entry reserved list, and the classic duplicate-id HTMLCollection vector (`<a id=x></a><a id=x name=y>`) passed unchanged. The list was also missing many high-value targets (`attributes`, `nodeName`, `getElementById`, `defaultView`, `implementation`, …). ## Fix - **Expanded `RESERVED_IDS`** with the missing global/document/node/traversal targets. - **Duplicate-id stripping** in `sanitize-core.ts`: the first occurrence of an id is kept; later elements sharing it have their `id` dropped, so named access resolves to a single node. - Documented that the `id`/`name` denylist is defense-in-depth, not a complete guarantee — fully untrusted content should drop `id`/`name` entirely. ## Verification - New tests: duplicate `id="x"` reduced to one occurrence; several newly-added reserved ids (`attributes`, `getElementById`, `defaultView`, `implementation`) are stripped. Both fail on the pre-fix code. - security/core/view suites: all pass. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
#200) ## Summary Fixes #180 (Low; correctness — silent no-op). `bq-on` decided "bare function reference vs. call" by testing whether the expression string contained `(`. This misfires on expressions where the paren is not the top-level call: ```html <button bq-on:click="items.find(x => x).handler"> ``` was treated as a full expression, evaluated once, and the returned `handler` was **never invoked** — the click silently did nothing. ## Fix Both `bq-on` sinks (`on.ts` and `on-modifiers.ts`) now share `runOnExpression`: always evaluate the expression via `evaluateRaw`, and if the result is a function, invoke it with the event; otherwise the evaluation itself was the side effect (`count.value++`, `handleClick($event)`). The string heuristic is removed. (`this` remains unbound for a function resolved from a member chain — use an explicit call when the receiver matters, as before.) ## Verification - New tests: a handler resolved from `items.find(matcher).handler` now fires; a bare `onClick` reference is invoked with the event. The inner-paren case is exactly what silently no-op'd on the pre-fix code. - Existing bq-on tests (bare ref, `handleClick($event)`, `count.value++`/`--`/`+=`) still pass. - view suites: 119 pass / 0 fail. `tsc --noEmit` + eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
#201) ## Summary Fixes #181 (Low; authz footgun by omission). Action/HTML routes received `options.middlewares`; the JSON loader endpoints received a **separate** `options.dataMiddlewares`. A developer who protects mutations with `middlewares: [auth]` but forgets `dataMiddlewares` exposed every route's `load()` output as **unauthenticated JSON** at `${dataPath}<route>` — the loader mirrors the same data an authenticated page renders, so this is a plausible authorization bypass introduced by omission. ## Fix `dataMiddlewares` now defaults to `middlewares` when unset (`options.dataMiddlewares ?? options.middlewares`). Pass `dataMiddlewares: []` to explicitly opt loader routes out of the action chain. Documented on the option. ## Verification - New tests: with `middlewares: [auth]` and no `dataMiddlewares`, the generated GET loader route inherits `[auth]`; with an explicit `dataMiddlewares: []` it opts out. The default-inheritance test fails on the pre-fix code. - server suites: all pass. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #168 (Medium hardening). `evaluate`/`evaluateRaw` run `new Function('$ctx', 'with($ctx){ return (expr); }')`. The proxy `has` trap returned `prop in target`, so inherited `Object.prototype` members resolved from the context and `constructor.constructor('…')()` reached the `Function` constructor — arbitrary code execution wherever `bq-*` attributes carry untrusted content. ## Fix The issue's suggested own-keys-only `has` trap is necessary but **not sufficient**: once the proxy declines `constructor`, `with` resolution falls through to the enclosing global scope, where the global object inherits `constructor` from `Object.prototype` and exposes `Function`/`eval`/`globalThis`. A bare `Function('…')()` escapes the same way. So the evaluator now **shadows** a denylist (`constructor`, `__proto__`, `prototype`, `Function`, `eval`, `globalThis`, `global`, `window`, `self`, `top`, `parent`): `has` reports them present (so they never fall through to the global scope) and `get` resolves them to `undefined` unless the context legitimately owns that property. Member access on the resulting `undefined` throws → `undefined`. Both the lazy (`evaluate`) and raw (`evaluateRaw`) paths are covered. Legitimate templates are unaffected: own context props (including ones shadowing a dangerous name), arithmetic, and method calls on context *values* (e.g. `name.toUpperCase()`) all still work. ## Verification - Tests: `constructor.constructor(…)()` on both `evaluate` and `evaluateRaw`, bare `Function(…)()` and `eval(…)`, plus positive cases (own props, method calls, own-prop shadowing a global name). The exploit tests fail on the pre-fix code. - Full suite: 2993 pass / 0 fail. `tsc --noEmit` + eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #176 (Low; CSS injection — no HTML breakout). The DOM-free `setStyle` concatenated `bq-style` property names and values into the inline `style` attribute with no CSS-level validation. Attribute escaping prevents HTML breakout, but an untrusted style value like `x;} body{display:none` injected extra declarations/rules (UI-redress/clickjacking, exfiltration via `background: url(...)`). ## Fix `setStyle` (pure renderer) now drops any declaration whose property name is not a valid CSS identifier (`--custom` or kebab-case) or whose value contains injection characters (`;`, `{`, `}`, `<`). The DOM-backed renderer was already safe — it writes via `CSSStyleDeclaration.setProperty`, which rejects malformed values. ## Verification - New tests across both backends: a malicious `width: 'x;} body{display:none'` is dropped; safe declarations (`color`, `marginTop`) pass through. The malicious case fails on the pre-fix pure renderer. - SSR suites: all pass. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Jonas Pfalzgraf <[email protected]> Co-authored-by: Claude Fable 5 <[email protected]>
…ll (#198) ## Summary Fixes #178 (Low; correctness). With `{ leading: true, trailing: true }`, a **single** call double-invoked: the leading branch invoked with `preservePending = trailing` (keeping `pendingArgs`), then `trailingTrigger` saw `pendingArgs` still set and invoked again. One call → two invocations. ## Fix The leading invoke now clears `pendingArgs`. A subsequent call within the window re-sets `pendingArgs` (the leading branch is skipped once `leadingDone`), so the trailing edge still fires — but only when the function was called more than once, matching lodash semantics. ## Verification - Existing multi-call test (`fn(1); fn(2); fn(3)` → `[1, 3]`) still passes. - Rewrote the single-call test that previously codified the bug: `fn(1)` now yields `[1]`, not `[1, 1]`. It fails on the pre-fix code. - function/utils/core suites: all pass. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #169 (Medium; direct credential-theft impact). The session-id cookie (the sole bearer credential) and the CSRF secret cookie (embeds the raw secret in signed mode) both defaulted to **no `Secure`** flag. On any plaintext-HTTP hop — an HTTP→HTTPS redirect, a misconfigured proxy — a network MITM could read them and hijack the session or forge matching CSRF tokens. `sameSite: 'lax'` does not compensate. ## Fix Both middlewares now default `secure: true` with an explicit opt-out for local HTTP dev: ```ts secure: options.cookie?.secure ?? true ``` Placed after the `...options.cookie` spread so a user's explicit `secure: false` still wins. Session (`session.ts`, including the destroy/expire path via shared `baseCookie`) and CSRF (`csrf.ts`) are covered. Doc comments updated. ## Verification - Strengthened the existing "secure-by-default" session test (it never actually asserted `Secure`) + new opt-out test. - New CSRF tests: `Secure` present by default, absent when `secure: false`. - `bun test` server suites: all pass. `tsc --noEmit` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <[email protected]>
## Summary Fixes #164 (High). `bq-bind` wrote runtime data to any attribute with no validation: - `<a bq-bind:href="link">` with `link = "javascript:alert(document.cookie)"` → clickable script execution - `<div bq-bind:onclick="h">` → registered a live inline handler - `<iframe bq-bind:srcdoc="msg">` (client **and** both SSR backends) → attribute-encoding is decoded by the browser and parsed as a full HTML document → script execution ## Fix New shared guard [`src/security/bind-guard.ts`](src/security/bind-guard.ts) (`checkBoundAttribute`), reused by all three sinks (client directive, pure SSR renderer, legacy DOM SSR renderer): - **`on*` names**: never written; client logs a warning - **`srcdoc`**: treated as an HTML sink — sanitized via `sanitizeHtml` (client) / `sanitizeHtmlForSSR` (SSR) - **URL attributes** (`href", `src`, `xlink:href`, `formaction`, `action`, `poster`, `background`, `cite`, `data`) + `srcset`: values with dangerous protocols (after control-char/zero-width normalization) are dropped Boolean/null semantics of `bq-bind` are unchanged; safe values pass through as before. ## Verification - New client tests (href-drop + re-set on safe value, onclick never written, srcdoc sanitized) and SSR tests across both backends (href, onclick, srcdoc, safe-URL passthrough). - Confirmed 5 of the new tests fail without the src changes. - Full suite: 2997 pass / 0 fail. `tsc --noEmit` + eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Jonas Pfalzgraf <[email protected]> Co-authored-by: Claude Fable 5 <[email protected]>
- @typescript-eslint/eslint-plugin and @typescript-eslint/parser upgraded to ^8.63.0 - eslint upgraded to ^10.6.0 - prettier upgraded to ^3.9.4 - typedoc upgraded to ^0.28.20 - vite upgraded to ^8.1.3
Security-and-correctness patch closing a full-codebase audit (issues #162–#181). No breaking changes, no module status transitions. - package.json: 1.15.0 → 1.15.1 - CHANGELOG.md: new [1.15.1] section (Security + Fixed) and TOC entry - docs: new release-notes/1.15.1 page, nav + index entries - docs/guide/security.md: document the new trustedHtmlForSink() helper Co-Authored-By: Claude Fable 5 <[email protected]>
📝 WalkthroughWalkthroughThis PR adds the v1.15.1 release material and hardens HTML sinks, attribute binding, expression evaluation, reactive lifecycles, and server cookie/middleware defaults. It also updates docs and tests to cover the new behavior. ChangesSecurity Hardening
Estimated code review effort: 4 (Complex) | ~75 minutes Reactive and Expression Hardening
Estimated code review effort: 4 (Complex) | ~70 minutes Server Defaults
Estimated code review effort: 2 (Simple) | ~15 minutes Release Metadata
Estimated code review effort: 1 (Trivial) | ~5 minutes Possibly related issues
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…rowser support table
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/view/evaluate.ts (1)
278-300: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winAOT
evaluateRawstill needs the hardened proxy
compiledRegistryentries are emitted as direct property reads on the context, soevaluateRaw’s AOT path can still reach inherited members likeconstructoron a raw object while the runtime fallback is hardened. Wrap the compiled call withcreateHardenedContext(context)so both paths enforce the same sandboxing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/view/evaluate.ts` around lines 278 - 300, The AOT path in evaluateRaw is bypassing the hardened sandbox because compiledRegistry entries are invoked with the raw context. Update evaluateRaw so the compiled branch also passes createHardenedContext(context) into the compiled function, matching the runtime fallback and keeping both paths equally protected from inherited members like constructor.
🧹 Nitpick comments (1)
tests/security.test.ts (1)
101-106: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winPin the fallback output more directly.
These assertions can pass without proving the mXSS text fallback ran. Add an assertion for the escaped decoded payload so this catches fallback regressions.
Proposed stronger assertion
const result = String(sanitizeHtml(payload)); + expect(result).toContain('<img src=x onerror=alert(1)>'); expect(result).not.toContain('<img');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/security.test.ts` around lines 101 - 106, The current checks in security.test.ts only verify that no img element survives sanitization, but they do not confirm the mXSS fallback path actually produced the escaped text output. Update the test around sanitizeHtml and the host.innerHTML parsing to assert the escaped decoded payload directly from the result, so regressions in the fallback behavior are caught; use the sanitizeHtml payload setup and the host query logic as the anchor points.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/security/trusted-types.ts`:
- Around line 71-83: The public API trustedHtmlForSink is exported via
src/security/index.ts but its JSDoc lacks the required `@example` block. Update
the comment above trustedHtmlForSink in src/security/trusted-types.ts to include
a concise usage example showing assignment to a DOM HTML sink, and keep the
existing description aligned with the exported API.
In `@src/store/utils.ts`:
- Around line 44-51: The deepClone guard is over-broad and drops legitimate own
data properties named constructor or prototype, not just the dangerous __proto__
key. Update isPrototypePollutionKey and the Object.keys loop in deepClone so
only __proto__ is skipped, while constructor and prototype are copied as normal
data properties. Add or adjust tests around deepClone in store/utils.ts to cover
own enumerable constructor/prototype fields and confirm they are preserved.
In `@src/view/compiler/expression.ts`:
- Around line 139-146: The decimal branch of NUMERIC_LITERAL_RE in expression.ts
is too permissive and still accepts leading-zero forms like 007 and 01.5.
Tighten the regex so the decimal literal path rejects non-zero-leading
integers/fractions with a leading 0, and update the bail-out coverage around
scanNumericLiteral to include cases like bail('007') and bail('01.5') so the
strict/module syntax errors are caught.
In `@src/view/evaluate.ts`:
- Around line 138-227: The current hardening in
createLazyContext/createHardenedContext only shadows bare identifiers via
hardenedHas and isShadowedGlobal, but object-member chains can still reach
Function through properties like constructor on reachable values. Update the
sandbox so member access is also constrained, either by deep-wrapping exposed
objects/proxies or by denying dangerous inherited members during property reads,
and verify expressions like value.constructor.constructor(...) and
this.constructor.constructor(...) cannot escape.
---
Outside diff comments:
In `@src/view/evaluate.ts`:
- Around line 278-300: The AOT path in evaluateRaw is bypassing the hardened
sandbox because compiledRegistry entries are invoked with the raw context.
Update evaluateRaw so the compiled branch also passes
createHardenedContext(context) into the compiled function, matching the runtime
fallback and keeping both paths equally protected from inherited members like
constructor.
---
Nitpick comments:
In `@tests/security.test.ts`:
- Around line 101-106: The current checks in security.test.ts only verify that
no img element survives sanitization, but they do not confirm the mXSS fallback
path actually produced the escaped text output. Update the test around
sanitizeHtml and the host.innerHTML parsing to assert the escaped decoded
payload directly from the result, so regressions in the fallback behavior are
caught; use the sanitizeHtml payload setup and the host query logic as the
anchor points.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1f17d37e-53b7-4543-9178-84da5a9e914b
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (52)
CHANGELOG.mddocs/.vitepress/config.tsdocs/guide/security.mddocs/release-notes/1.15.1.mddocs/release-notes/index.mdpackage.jsonsrc/concurrency/scheduling.tssrc/core/collection.tssrc/core/dom.tssrc/core/utils/function.tssrc/full.tssrc/i18n/icu.tssrc/i18n/translate.tssrc/reactive/async-data.tssrc/reactive/effect.tssrc/reactive/persisted.tssrc/reactive/scope.tssrc/security/bind-guard.tssrc/security/constants.tssrc/security/index.tssrc/security/sanitize-core.tssrc/security/sanitize.tssrc/security/trusted-types.tssrc/server/csrf.tssrc/server/file-routes.tssrc/server/session.tssrc/ssr/head.tssrc/ssr/render.tssrc/ssr/renderer.tssrc/store/create-store.tssrc/store/utils.tssrc/view/compiler/expression.tssrc/view/directives/bind.tssrc/view/directives/html.tssrc/view/directives/lightweight.tssrc/view/directives/on-modifiers.tssrc/view/directives/on-shared.tssrc/view/directives/on.tssrc/view/evaluate.tstests/concurrency-stable.test.tstests/i18n.test.tstests/security.test.tstests/server-file-routes.test.tstests/server-stable.test.tstests/signal.test.tstests/ssr-runtime.test.tstests/ssr-stable.test.tstests/ssr.test.tstests/store.test.tstests/utils-function-extras.test.tstests/view-compiler.test.tstests/view.test.ts
- security: add an @example to the trustedHtmlForSink JSDoc (public-API guideline). - store: deepClone now only special-cases __proto__ (defined as an own data property to avoid the prototype-reassigning setter) and copies genuine own data keys named constructor/prototype normally, instead of silently dropping them. - view/compiler: NUMERIC_LITERAL_RE rejects legacy leading-zero decimals (007, 01.5) that are SyntaxErrors in the emitted ES module. - view: block member access to constructor/prototype/__proto__ in the runtime evaluator and the AOT compiler. Identifier shadowing alone did not stop foo.constructor.constructor('…')() reaching Function through any reachable object; a shared hasDangerousMemberAccess() guard now rejects dotted/optional-chaining/bracket-string access on both paths. Documented as defense-in-depth (computed bracket access remains out of scope; the AOT compiler is the fully CSP-safe path). Co-Authored-By: Claude Fable 5 <[email protected]>
|
Addressed all four review comments in 418d3dd:
Full suite: 3048 pass / 0 fail; @coderabbitai review |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
fd . -e ts | rg -n "trusted-types.ts$|store/utils.ts$|expression.ts$|evaluate.ts$" src/security src/store src/view/compiler src/view 2>/dev/null
echo "---trusted-types.ts---"
sed -n '1,40p' src/security/trusted-types.ts
echo "---store/utils.ts deepClone---"
rg -n "deepClone|__proto__|constructor|prototype" src/store/utils.ts
echo "---compiler/expression.ts NUMERIC_LITERAL_RE---"
rg -n "NUMERIC_LITERAL_RE" -A5 src/view/compiler/expression.ts
echo "---evaluate.ts hasDangerousMemberAccess---"
rg -n "hasDangerousMemberAccess" -A15 src/view/evaluate.ts | head -100Length of output: 907 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "---trusted-types.ts---"
cat -n src/security/trusted-types.ts
echo "---store/utils.ts---"
cat -n src/store/utils.ts
echo "---compiler/expression.ts NUMERIC_LITERAL_RE---"
rg -n "NUMERIC_LITERAL_RE" -A8 -B2 src/view/compiler/expression.ts
echo "---evaluate.ts hasDangerousMemberAccess---"
rg -n "hasDangerousMemberAccess" -A20 -B2 src/view/evaluate.tsLength of output: 13500 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "---dangerous-access.ts---"
fd -t f "dangerous-access" src | xargs cat -n
echo "---compiler use of hasDangerousMemberAccess---"
rg -n "hasDangerousMemberAccess" -B3 -A10 src/view/compiler/*.tsLength of output: 4972
Everything aligns with the description, and the reasoning/documentation added for the defense-in-depth boundaries is clear. Thanks for the thorough writeup — kicking off a fresh review pass now. ✏️ Learnings added
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/view/compiler/expression.ts`:
- Around line 150-151: The numeric literal scanner in scanNumericLiteral is
splitting exponent signs before NUMERIC_LITERAL_RE can validate them, causing
literals like 1e+2 and 1e-2 to be rejected. Update scanNumericLiteral in
src/view/compiler/expression.ts to allow an optional + or - only immediately
after e/E within a numeric token, while keeping + and - outside exponents as
separate operators so expressions like 0x1e + 2 still parse correctly.
In `@src/view/dangerous-access.ts`:
- Around line 27-32: The regex-based checks in dangerous-access.ts are too
permissive and miss normalized dangerous member names while also misclassifying
non-member text as optional chaining. Replace the raw matching in
DANGEROUS_DOT_ACCESS and DANGEROUS_BRACKET_ACCESS with a small linear scanner
that recognizes actual member-access tokens only, decodes static bracket-string
escapes, and compares the normalized property names against DANGEROUS_MEMBERS so
cases like escaped constructor/prototype names are caught correctly.
In `@tests/view.test.ts`:
- Around line 1503-1517: Restore the `console.error` stub in these tests so it
does not leak into later test cases; in the `evaluate`/`evaluateRaw` specs
within `tests/view.test.ts`, save the original `console.error` before overriding
it, then reset it in a `finally` block or move the cleanup to an `afterEach` for
the test group. Make sure the fix is applied around both escape-blocking tests
that currently stub `console.error`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 03efa4f9-e33b-41f7-9a80-99c00c740e3f
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
README.mdsrc/security/trusted-types.tssrc/store/utils.tssrc/view/compiler/expression.tssrc/view/dangerous-access.tssrc/view/evaluate.tstests/store.test.tstests/view-compiler.test.tstests/view.test.ts
✅ Files skipped from review due to trivial changes (1)
- README.md
🚧 Files skipped from review as they are similar to previous changes (3)
- src/security/trusted-types.ts
- tests/view-compiler.test.ts
- tests/store.test.ts
| const NUMERIC_LITERAL_RE = | ||
| /^(?:0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*|0[oO][0-7](?:_?[0-7])*|0[bB][01](?:_?[01])*|(?:0|[1-9](?:_?\d)*)(?:\.(?:\d(?:_?\d)*)?)?(?:[eE][+-]?\d(?:_?\d)*)?|\.\d(?:_?\d)*(?:[eE][+-]?\d(?:_?\d)*)?)$/; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify these are valid JavaScript numeric expressions.
node - <<'JS'
for (const src of ['1e+2', '1e-2', '0x1e + 2']) {
console.log(src, new Function(`return (${src});`)());
}
JSRepository: bQuery/bQuery
Length of output: 182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant file and locate the numeric literal scanner/validator.
git ls-files 'src/view/compiler/expression.ts' 'src/view/compiler/*' | sed -n '1,120p'
echo '--- outline ---'
ast-grep outline src/view/compiler/expression.ts --view expanded || true
echo '--- matches ---'
rg -n "NUMERIC_LITERAL_RE|scanNumericLiteral|numeric literal|hasDangerousMemberAccess|e[+-]" src/view/compiler/expression.tsRepository: bQuery/bQuery
Length of output: 2310
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the numeric-literal scanner and its use site.
sed -n '136,280p' src/view/compiler/expression.ts | cat -nRepository: bQuery/bQuery
Length of output: 5491
Keep exponent signs inside numeric literals (src/view/compiler/expression.ts:158). scanNumericLiteral() stops at +/-, so 1e+2 and 1e-2 get split before NUMERIC_LITERAL_RE runs and are rejected. Consume an optional sign only after e/E, so 0x1e + 2 still parses as binary +.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/view/compiler/expression.ts` around lines 150 - 151, The numeric literal
scanner in scanNumericLiteral is splitting exponent signs before
NUMERIC_LITERAL_RE can validate them, causing literals like 1e+2 and 1e-2 to be
rejected. Update scanNumericLiteral in src/view/compiler/expression.ts to allow
an optional + or - only immediately after e/E within a numeric token, while
keeping + and - outside exponents as separate operators so expressions like 0x1e
+ 2 still parse correctly.
| /** Dotted or optional-chaining member access: `.constructor`, `?.constructor`. */ | ||
| const DANGEROUS_DOT_ACCESS = new RegExp(`[.?]\\s*(?:${DANGEROUS_MEMBERS})\\b`); | ||
|
|
||
| /** String-literal bracket access: `['constructor']`, `["prototype"]`. */ | ||
| const DANGEROUS_BRACKET_ACCESS = new RegExp( | ||
| `\\[\\s*(['"\`])\\s*(?:${DANGEROUS_MEMBERS})\\s*\\1\\s*\\]` |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Demonstrate that escaped property names normalize to "constructor" in JS.
node - <<'JS'
const items = [];
console.log(new Function('items', "return items['constr\\\\u0075ctor'] === Array;")(items));
console.log(new Function('items', "return items.\\\\u0063onstructor === Array;")(items));
JSRepository: bQuery/bQuery
Length of output: 777
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the helper and its call sites.
git ls-files | rg '^src/view/dangerous-access\.ts$|^src/view/' || true
wc -l src/view/dangerous-access.ts
cat -n src/view/dangerous-access.ts
printf '\n--- search usages ---\n'
rg -n "DANGEROUS_DOT_ACCESS|DANGEROUS_BRACKET_ACCESS|dangerous-access" src -S
printf '\n--- parser probes ---\n'
node - <<'JS'
const probes = [
"items['constr\\u0075ctor']",
"items[\"constr\\u0075ctor\"]",
"items.\\u0063onstructor",
"items?.constructor",
"items ? constructor : 1",
"items?.['constr\\u0075ctor']",
];
for (const src of probes) {
try {
const fn = new Function('items', `return ${src};`);
const out = fn([]);
console.log(JSON.stringify({src, parsed: true, resultType: typeof out, result: out === undefined ? 'undefined' : String(out)}));
} catch (e) {
console.log(JSON.stringify({src, parsed: false, error: String(e)}));
}
}
JSRepository: bQuery/bQuery
Length of output: 4631
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- src/view/evaluate.ts ---'
cat -n src/view/evaluate.ts
printf '\n%s\n' '--- src/view/compiler/expression.ts ---'
cat -n src/view/compiler/expression.tsRepository: bQuery/bQuery
Length of output: 34631
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const DANGEROUS_MEMBERS = 'constructor|prototype|__proto__';
const DANGEROUS_DOT_ACCESS = new RegExp(`[.?]\\s*(?:${DANGEROUS_MEMBERS})\\b`);
const DANGEROUS_BRACKET_ACCESS = new RegExp(
`\\[\\s*(['"\`])\\s*(?:${DANGEROUS_MEMBERS})\\s*\\1\\s*\\]`
);
const samples = [
"items.constructor",
"items?.constructor",
"items ? constructor : 1",
"items['constructor']",
"items['constr\\u0075ctor']",
"items.\\u0063onstructor",
"items?.['constr\\u0075ctor']",
"items['proto\\u0074ype']",
"foo['__pr\\u006fto__']",
];
for (const s of samples) {
console.log(JSON.stringify({
s,
dot: DANGEROUS_DOT_ACCESS.test(s),
bracket: DANGEROUS_BRACKET_ACCESS.test(s),
any: DANGEROUS_DOT_ACCESS.test(s) || DANGEROUS_BRACKET_ACCESS.test(s),
}));
}
JSRepository: bQuery/bQuery
Length of output: 785
Parse member access instead of matching raw text.
The raw regex misses escaped static names that JavaScript normalizes, so foo['constr\u0075ctor'], foo.\u0063onstructor, and foo['__pr\u006fto__'] can still reach dangerous members. It also treats ? constructor in ternaries as optional chaining. A small linear scanner that decodes static bracket strings and matches only real . / ?. member tokens would close both gaps.
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 27-27: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp([.?]\\s*(?:${DANGEROUS_MEMBERS})\\b)
Note: [CWE-1333] Inefficient Regular Expression Complexity
(regexp-from-variable)
[warning] 30-32: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
\\[\\s*(['"\])\s*(?:${DANGEROUS_MEMBERS})\s*\1\s*\]`
)
Note: [CWE-1333] Inefficient Regular Expression Complexity
(regexp-from-variable)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/view/dangerous-access.ts` around lines 27 - 32, The regex-based checks in
dangerous-access.ts are too permissive and miss normalized dangerous member
names while also misclassifying non-member text as optional chaining. Replace
the raw matching in DANGEROUS_DOT_ACCESS and DANGEROUS_BRACKET_ACCESS with a
small linear scanner that recognizes actual member-access tokens only, decodes
static bracket-string escapes, and compares the normalized property names
against DANGEROUS_MEMBERS so cases like escaped constructor/prototype names are
caught correctly.
Source: Coding guidelines
| it('blocks the object-member escape via a reachable context value (#202)', () => { | ||
| console.error = () => {}; | ||
| // `foo` is a legit own context value, but `foo.constructor.constructor` | ||
| // reaches Function without resolving a bare identifier — must be blocked. | ||
| expect(evaluate("items.constructor.constructor('return 2')()", { items: [] })).toBeUndefined(); | ||
| expect(evaluateRaw("items.constructor.constructor('return 2')()", { items: [] })).toBeUndefined(); | ||
| expect(evaluate("name.constructor('return 2')()", { name: 'x' })).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('blocks optional-chaining and bracket member escapes (#202)', () => { | ||
| console.error = () => {}; | ||
| expect(evaluate("items?.constructor?.constructor('x')()", { items: [] })).toBeUndefined(); | ||
| expect(evaluate("items['constructor']['constructor']('x')()", { items: [] })).toBeUndefined(); | ||
| expect(evaluate('items.__proto__', { items: [] })).toBeUndefined(); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Restore console.error after stubbing it.
These tests leave console.error as a no-op for the rest of the test process. Wrap the stub in try/finally or restore it in an afterEach so later failures are not hidden.
Proposed local pattern
it('blocks the object-member escape via a reachable context value (`#202`)', () => {
+ const originalConsoleError = console.error;
console.error = () => {};
- // `foo` is a legit own context value, but `foo.constructor.constructor`
- // reaches Function without resolving a bare identifier — must be blocked.
- expect(evaluate("items.constructor.constructor('return 2')()", { items: [] })).toBeUndefined();
- expect(evaluateRaw("items.constructor.constructor('return 2')()", { items: [] })).toBeUndefined();
- expect(evaluate("name.constructor('return 2')()", { name: 'x' })).toBeUndefined();
+ try {
+ // assertions...
+ } finally {
+ console.error = originalConsoleError;
+ }
});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/view.test.ts` around lines 1503 - 1517, Restore the `console.error`
stub in these tests so it does not leak into later test cases; in the
`evaluate`/`evaluateRaw` specs within `tests/view.test.ts`, save the original
`console.error` before overriding it, then reset it in a `finally` block or move
the cleanup to an `afterEach` for the test group. Make sure the fix is applied
around both escape-blocking tests that currently stub `console.error`.
Release 1.15.1 — Security & correctness patch
Promotes
devtomainfor the 1.15.1 patch release. This closes the findings of a full-codebase security & correctness audit (issues #162–#181).No breaking changes. No module status transitions. Every 1.15.0 and earlier API continues to work unchanged — upgrading is a drop-in.
Security fixes
sanitizeHtmlnow HTML-escapes its text result (reachable through every HTML sink)bq-texton raw-text elements (textarea/title) escaped in the pure renderer (stored XSS)bq-bindguardson*/URL/srcset/srcdocvalues via a shared bind-guardwith-scoped evaluator shadows dangerous globals — closesconstructor.constructor(…)()RCEnew Function())SecuretrustedHtmlForSink())bq-styledeclarations validated (CSS injection)deepCloneguards__proto__Correctness fixes
$subscribesnapshots subscribers (no skip on unsubscribe-during-notify)useFetch/useAsyncDataabort the superseded requestdeferred()/persistedSignal()(+effectScope(detached))titleTemplateinserts the title literally (no$&mangling)debounce({leading,trailing})no longer double-invokes a single callbq-oninvokes handlers via evaluation, not a paren heuristicAdditive APIs (backwards-compatible)
trustedHtmlForSink()—@bquery/bquery/security(also on/full)effectScope(detached?)—@bquery/bquery/reactivedeferred()handle now exposesdispose()Migration notes
Two behavioural defaults tightened, both with explicit opt-outs:
Secure→ passcookie: { secure: false }for local HTTP dev.middlewares→ passdataMiddlewares: []to keep them unauthenticated.Release chores
package.json: 1.15.0 → 1.15.1CHANGELOG.md: new[1.15.1]section (Security + Fixed) and TOC entryrelease-notes/1.15.1page, nav + index entries,trustedHtmlForSink()documented in the Security guideValidation
bun test: 3043 pass / 0 failtsc --noEmit(src + tests): cleanbun run build: succeedscheck:stability,check:doc-exports(security 11/11),check:full-bundle: pass🤖 Generated with Claude Code
Summary by CodeRabbit
bq-bind/bq-html/bq-texthandling against XSS, dangerous URL protocols, DOM clobbering, and prototype-chain attacks.useFetchaborting.debounceleading+trailing behavior and various SSR/template and i18n edge cases.trustedHtmlForSinkhelper for Trusted Types HTML sink assignments.effectScope(detached)with manual stopping and enhanceddeferred()to support explicit disposal.