Skip to content

fix(tiers 4–5): an XSS in the attribute name, x shot broken on every route, and a config page describing a framework we do not ship - #294

Merged
sebyx07 merged 2 commits into
mainfrom
fix/sweep-two-tier4
Aug 22, 2026
Merged

fix(tiers 4–5): an XSS in the attribute name, x shot broken on every route, and a config page describing a framework we do not ship#294
sebyx07 merged 2 commits into
mainfrom
fix/sweep-two-tier4

Conversation

@sebyx07

@sebyx07 sebyx07 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fourth of eight PRs executing docs/plans/2026/08/21/101-deep-dive-sweep-two — slices 05 and 06, plus decision D3. Builds on #288, #291, #292. Four agents, disjoint sets, one checkout.

Carries 3 more of the breaking changes batched for 8.0.0 (now 6 total, up from the plan's 4).

Security: two ways to inject a handler through an attribute NAME

<div {...row} />
Input Emitted, before
{ ONERROR: 'alert(1)' } ONERROR="alert(1)" — the handler test was name.startsWith('on'), case-sensitive, while the two lines beside it folded case
{ 'q onmouseover=alert(1) r': 'ok' } q onmouseover=alert(1) r="ok" — the attribute name was never validated at all, so a key carries a second attribute out of the quotes

Names now match /^[A-Za-z_:][-A-Za-z0-9_:.]*$/ and the handler test folds case. Both are reachable by spreading a JSON body or a JSONB column into JSX.

head.ts's renderTag had the identical hole — found beyond the brief, in the same package — emitting <meta name="q" r onmouseover=alert(1) s="ok"> from an attrs key. It now shares the predicate. One predicate, never two.

x shot reported failure on every route of every app

pageOverTarget spread the frame and overrode goto/screenshot but not url, and the frame seeds lastUrl at construction. So ScrapePage.url() answered about:blank before a navigation, after it, and after reading text — and the CLI compares it as finalUrl, so every run exited non-zero with "redirected to about:blank".

One line. No test anywhere had ever called ScrapePage.url(), which is exactly why it shipped. The new parity test covers fake, fixture and the CDP code path.

The config page documented a framework that does not exist

wiki/Configuration.md — the wiki is this project's only public documentation surface — documented roughly 40 config fields that exist on no declaration: auth.providers, auth.session.*, auth.passkeys, auth.trustedOrigins, jobs.retry.*, jobs.retention.*, cache.redis.*, all eight seo.*, all seven budgets.*, storage.driver/bucket/dir, otel.endpoint/sampling.

Its top-level app.config.ts example did not compile — proven, not asserted: the fence was extracted verbatim and run through tsc against the repo's own config.

before: .x/fence-check/app.config.ts(19,15): error TS2353:
        'urlEnv' does not exist in type 'Input<DatabaseConfig>'
after:  tsc-exit=0

This framework's stated primary developer is an agent, and an agent copies the example. The page is now derived from the interfaces; capabilities that genuinely do not exist are stated as gaps rather than documented as features. The documented session cookie name was also wrong — 'x_session' where the real one is '__Host-x_session', and that prefix is the session-fixation defence.

Breaking changes

Change Migration
pwa.installPrompt, auth.afterSignInPath, ai.modelEnv deleted delete the key; there is no replacement key because there was never a behaviour
@ultimat3/manifest drops canonical use canonicalJson from @ultimat3/core
@ultimat3/render drops matchRoute, RouteMatch @ultimat3/http's trie is the live matcher; render's had zero consumers

ai.modelEnv's own doc comment argued for its deletion:

"Env key for the model id, so no model string is baked into the image — an intention, not a behaviour… nothing consumes the merged value… So the exact thing this key exists to prevent — a model string baked into the image — is what actually happens."

Also fixed

  • A fired wedge watchdog never quit a remote browsershutdown() returned early on the same flag watch() sets when it fires. On remoteBrowser(), which the driver calls "the PRIMARY production path", browser.process() is null, so nothing ended the remote session. Measured { quits: 0, kills: 1 }.
  • X_SCRAPE_WATCHDOG_STOPPED (new, terminal) — an injected clock whose sleep() rejects killed the guard's loop and left the run unwatched. Separate from X_SCRAPE_WEDGED deliberately: that one means the page stopped answering and would send a reader to debug a page that is fine. Terminal where its sibling is retryable, because attempt 2 reaches the same clock identically — retrying buys five browser launches that die at the first poll, and on an authenticated target five arrivals at a login.
  • The 3rd and 4th canonical serialisers deleted. manifest's fed buildId and the contract-diff equality, so a -0/NaN/Date fold could make a breaking API change diff as "no change" and ship silently. Two contracts differing only by -0 vs 0 hashed identically before; they differ now and the diff reports it. buildId unchanged for both tracked apps.
  • Two divergent formatBytes (render's had no mb branch: 5120kb vs 5mb) · mail's memory driver stamped at: new Date() · mcp recompiled a RegExp per validation · seven more instanceof-on-a-caught-value sites · @ultimat3/testing created a temp dir in every test process importing its barrel, even for expect alone, and removed none · two JSX probes over one globalThis.React.

Escaped rather than absorbed

  • cache.tiers accepts 'memo'/'shared'/'isr' and the cache package has none of them — two vocabularies for one set of rungs #293cache.tiers accepts 'memo' | 'shared' | 'isr' and @ultimat3/cache has none of those names; 'isr' appears in the package only as an error label. Two vocabularies for one set of rungs, which is the exact defect render-modes exists to catch.
  • A FIFTH canonical serialiser in scripts/lib/framework-manifest.ts, hashing the framework's own manifest → routed to the gate-scripts slice.
  • scripts/error-render.ts cannot see the catch binding class at all — proven: green before and after a seven-site fix. Fifteen sites across five packages have been fixed by hand this sweep and no gate found one.
  • No checker compiles a wiki fencereadme-fences.ts scans packages/*/README.md only. That is why the config example rotted silently.

Gate

bun run verify14 of 19 passed, 5 skipped, exit 0.

One red first, and an interesting one: the XSS example's attack string begins x , so doc-commands.ts parsed it as an invocation of the x CLI and reported X_DOC_COMMAND_UNKNOWN. An allowance would have been the wrong fix — that entry means "this sentence is about a command not existing" — so the example key was renamed instead. A real false-positive class that only appears when documentation quotes hostile input.

🤖 Generated with Claude Code

https://claude.ai/code/session_0135KMN4Tfq1xhMwts1FNvis


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

sebyx07 and others added 2 commits August 22, 2026 13:55
…and 40 fictional config keys

Three more breaking changes for 8.0.0, two security fixes, and a documentation
page that described a framework this repo does not ship.

BREAKING — three config fields deleted: pwa.installPrompt, auth.afterSignInPath,
ai.modelEnv. Each declared, defaulted, merged, and read by nothing. modelEnv's
own doc comment already argued for its deletion: "an intention, not a behaviour
... So the exact thing this key exists to prevent — a model string baked into
the image — is what actually happens."

BREAKING — @ultimat3/manifest drops `canonical`; @ultimat3/render drops
`matchRoute` and `RouteMatch` (two exported matchers with different precedence;
http's trie is the live one and render's had zero consumers).

Security, two holes in one function. `attributePair` tested
`name.startsWith('on')` CASE-SENSITIVELY while the lines beside it folded case,
so ONERROR="alert(1)" went out on the wire; and the attribute NAME was never
validated at all, so an object key could carry a whole second attribute out of
the quotes. Both reachable by `<div {...row} />` over a JSON body or a JSONB
column. `head.ts`'s renderTag had the identical hole and now shares the
predicate — one predicate, never two.

`x shot` reported failure on every route of every app. `pageOverTarget` spread
the frame and overrode goto/screenshot but not `url`, and the frame seeds
`lastUrl` at construction — so `ScrapePage.url()` answered `about:blank` before
a navigation, after it, and after reading text. The CLI compares that as
`finalUrl`. One line. No test had ever called `ScrapePage.url()`, which is why
it shipped.

A fired wedge watchdog never quit a remote browser: shutdown() returned early on
the same flag watch() sets when it fires. On remoteBrowser() — "the PRIMARY
production path" — `browser.process()` is null, so nothing ended the remote
session. New terminal code X_SCRAPE_WATCHDOG_STOPPED covers the adjacent case
where an injected clock's sleep() rejects and leaves the run unwatched; a
separate code from X_SCRAPE_WEDGED because that one means the page stopped
answering and would send a reader to investigate a page that is fine.

The third and fourth copies of the canonical serialiser are gone, per
canonical-json.ts's own "never add a fourth copy". manifest's fed buildId AND
the contract-diff equality, so a -0/NaN/Date fold could make a breaking API
change diff as "no change" and ship silently. Verified: two contracts differing
only by -0 vs 0 hashed identically before and differ now. A FIFTH copy remains
in scripts/lib/framework-manifest.ts, routed to the gate-scripts slice.

wiki/Configuration.md documented roughly 40 config fields that exist on no
declaration — auth.providers, auth.session.*, auth.passkeys, jobs.retry.*,
jobs.retention.*, cache.redis.*, all eight seo.*, all seven budgets.*,
storage.*, otel.* — and its top-level app.config.ts example did not compile
(TS2353 on database.urlEnv, verified by extracting the fence and running tsc).
The page is now derived from the interfaces and the example compiles. The
documented session cookie name was wrong too: 'x_session' where the real one is
'__Host-x_session', whose prefix is the session-fixation defence. No checker
compiles a wiki fence — routed to the gate-scripts slice.

Also: two divergent formatBytes copies unified in core (render's had no `mb`
branch, so a 5 MiB route read 5120kb); mail's memory driver stamped
`at: new Date()`, the one unseamed clock in the package; mcp recompiled a RegExp
per validation; seven more instanceof-on-a-caught-value sites in ai and mail;
@ultimat3/testing created a temp dir in every test process that imported its
barrel, for `expect` alone, and removed none; two JSX probes kept separate depth
counters over one globalThis.React.

Refs docs/plans/2026/08/21/101-deep-dive-sweep-two/{05-tier4-render-pwa-mail-manifest-ai,06-tier5-scraping-testing}.md
Refs #293

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0135KMN4Tfq1xhMwts1FNvis
Both tracked apps set `pwa.installPrompt`, and the social demo also set
`auth.afterSignInPath`. Neither key was ever read by anything, so removing them
changes no behaviour — but leaving them would fail excess-property checking
against the narrowed interfaces the parent commit ships, which is the whole
point of deleting a declaration rather than documenting it as dead.

`examples/dummy` also carried `ai.modelEnv: 'ANTHROPIC_MODEL'`, which selected
no model. It is gone with the field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0135KMN4Tfq1xhMwts1FNvis
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 52 minutes

Limit details: You’ve used the included review currently available. Your 71 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

You’re in a promotional period — use the checkbox below to run this review for free:

  • Run review for free

On-demand reviews are free for the next 29 days. After that, they cost $0.25 per reviewed file.

How can I continue?

Run this review now using the option above, or comment @coderabbitai review --use-credits.

You can also wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 96df9d81-467c-4316-ae76-cc27a11f69ef

📥 Commits

Reviewing files that changed from the base of the PR and between 89cdc98 and e179ebd.

📒 Files selected for processing (82)
  • CHANGELOG.md
  • docs/plans/2026/08/21/101-deep-dive-sweep-two/08-scaffold-dx.md
  • docs/plans/2026/08/21/101-deep-dive-sweep-two/09-gate-scripts.md
  • dummy/social-media-clone/app.config.ts
  • examples/dummy/app.config.ts
  • framework.manifest.json
  • packages/admin/CLAUDE.md
  • packages/admin/src/inert-jsx.test.ts
  • packages/admin/src/inert-jsx.ts
  • packages/ai/CLAUDE.md
  • packages/ai/src/agent.ts
  • packages/ai/src/factory-ordinal.test.ts
  • packages/ai/src/hive.ts
  • packages/ai/src/openai-wire.ts
  • packages/ai/src/prompt.test.ts
  • packages/ai/src/prompt.ts
  • packages/ai/src/remote-embedder.test.ts
  • packages/ai/src/remote-embedder.ts
  • packages/ai/src/wire.ts
  • packages/cli/src/templates/scaffold-config.test.ts
  • packages/core/CLAUDE.md
  • packages/core/src/config.test.ts
  • packages/core/src/config.ts
  • packages/core/src/format-bytes.test.ts
  • packages/core/src/format-bytes.ts
  • packages/core/src/index.ts
  • packages/mail/CLAUDE.md
  • packages/mail/README.md
  • packages/mail/src/driver-resend.test.ts
  • packages/mail/src/driver-resend.ts
  • packages/mail/src/driver-smtp.ts
  • packages/mail/src/driver.test.ts
  • packages/mail/src/driver.ts
  • packages/mail/src/index.ts
  • packages/manifest/CLAUDE.md
  • packages/manifest/README.md
  • packages/manifest/src/build.test.ts
  • packages/manifest/src/build.ts
  • packages/manifest/src/diff-operations.ts
  • packages/manifest/src/diff-routes.ts
  • packages/manifest/src/diff-work.ts
  • packages/manifest/src/emit.test.ts
  • packages/manifest/src/emit.ts
  • packages/manifest/src/index.ts
  • packages/mcp/CLAUDE.md
  • packages/mcp/src/validate-args.test.ts
  • packages/mcp/src/validate-args.ts
  • packages/pwa/CLAUDE.md
  • packages/pwa/src/index.ts
  • packages/pwa/src/precache.test.ts
  • packages/pwa/src/precache.ts
  • packages/render/CLAUDE.md
  • packages/render/README.md
  • packages/render/src/head.test.ts
  • packages/render/src/head.ts
  • packages/render/src/html.test.ts
  • packages/render/src/html.ts
  • packages/render/src/index.test.ts
  • packages/render/src/index.ts
  • packages/render/src/islands.test.ts
  • packages/render/src/islands.ts
  • packages/render/src/registry.test.ts
  • packages/render/src/registry.ts
  • packages/scraping/CLAUDE.md
  • packages/scraping/README.md
  • packages/scraping/src/driver-parity.test.ts
  • packages/scraping/src/error-throws.ts
  • packages/scraping/src/errors.test.ts
  • packages/scraping/src/errors.ts
  • packages/scraping/src/page-over-target.ts
  • packages/scraping/src/scrape.ts
  • packages/scraping/src/watchdog.test.ts
  • packages/scraping/src/watchdog.ts
  • packages/testing/CLAUDE.md
  • packages/testing/src/fixture-island-cleanup.test.ts
  • packages/testing/src/fixture-island.ts
  • packages/ui/CLAUDE.md
  • packages/ui/package.json
  • packages/ui/src/jsx-probe.ts
  • wiki/Configuration.md
  • wiki/Error-Codes.md
  • wiki/Upgrading.md
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sweep-two-tier4

Comment @coderabbitai help to get the list of available commands.

@sebyx07
sebyx07 merged commit 84b8199 into main Aug 22, 2026
36 checks passed
@sebyx07
sebyx07 deleted the fix/sweep-two-tier4 branch August 22, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant