Skip to content

chore(deps): resync the npm lockfile, and bound the name a preference is stored under - #921

Merged
KrasimirKralev merged 6 commits into
betafrom
chore/security-alerts-2026-09-18
Sep 18, 2026
Merged

KrasimirKralev merged 6 commits into
betafrom
chore/security-alerts-2026-09-18

Conversation

@KrasimirKralev

@KrasimirKralev KrasimirKralev commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Why

A triage of the repository's Security tab β€” 17 open Dependabot alerts, 4 open code-scanning alerts, 0 secret-scanning alerts β€” turned up two things worth a branch. The rest of the list is already remediated on beta and is open only because alerts are computed against the default branch, main.

What this PR carries

1. chore(deps): the npm lockfile had drifted out of sync with package.json.
package.json asks for playwright/@playwright/test ^1.63.0, bun.lock carries 1.63.0, package-lock.json still pinned 1.62.1 and still declared ^1.62.1 in its root entry β€” so npm ci refused the tree outright:

npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json or npm-shrinkwrap.json are in sync.
npm error Invalid: lock file's @playwright/test@1.62.1 does not satisfy @playwright/test@1.63.0
npm error Invalid: lock file's playwright@1.62.1 does not satisfy playwright@1.63.0
npm error Invalid: lock file's playwright-core@1.62.1 does not satisfy playwright-core@1.63.0

CI and install.sh both build with bun install --frozen-lockfile, and Dependabot's npm ecosystem is open-pull-requests-limit: 0, which is why nothing caught it and why it would have happened again on the next grouped bump. It matters because package-lock.json is the manifest GitHub's dependency graph audits β€” thirteen of the fifteen still-open Dependabot alerts are filed against it β€” so a copy that no longer matches package.json makes those verdicts describe a tree nobody ships, and a contributor reaching for npm cannot install at all. Regenerated with npm install, then pinned by a unit test (src/tests/unit/lockfiles-in-sync.test.ts) that makes npm's own first check β€” packages[""] against package.json β€” an invariant of the suite that already runs on every PR.

2. fix(preferences): bound the name a preference may be stored under.
The only rule on a preference NAME was the route's prefix test, so everything after ui_ was free β€” any length, any character. Both doors put that caller-supplied name straight onto an object, and the write one lands in config.json:

result[key] = allConfig[`${PREFERENCE_KEY_PREFIX}${key}`];   // GET
entries[`${PREFERENCE_KEY_PREFIX}${key}`] = value;           // POST

So a caller with a session could park unbounded arbitrary names in the owner's store β€” up to the 64 KB a value may be, per name, with nothing bounding how many. Code scanning has been flagging both writes (js/remote-property-injection, alerts #300 and #301); they are the two of the four open code-scanning alerts that beta had not already closed.

safePreferenceKey is the rule, in the module that owns the preference rules: a length bound (128) and an alphabet, with the name rebuilt character by character rather than tested and passed through β€” the same shape as safeProjectId in code-projects.ts, and for the same reason, that a .test() guard leaves the caller's own string in play at the sink. Applied at every door, not only the two the analyser named: the route, validatePreference (which carries it to the read path so a name stored before this existed stops being served), and sanitizePreferenceValue (which carries it to the machine door, sanitizePreferenceWrites β€” app install/uninstall and the webapp registry). The MCP preferences_set tool needs nothing: it posts through this route with a key bounded by its own enum.

The two ways a name can fail are told apart, because they are not the same thing. A name this door does not own β€” one with no preference prefix β€” is skipped, as it always has been; the caller was not asking this route to store it. A name with the prefix but spelled impossibly is this door's and is refused with a 400, like an impossible value, and nothing in the request lands. That is the contract the route's own header comment already stated, and the desktop's writers depend on it: InstalledAppSettings renders "Saved" over preferencesRes.ok and usePreferenceWriter does not read the body at all, so a silently skipped write would have been drawn as a saved one. The name is never quoted back β€” the body is both returned and logged.

The all=1 read sink (result[key.slice(…)] = value) is deliberately left unrebuilt: its key comes from the store, not from the request, and sanitizePreferences now drops any stored name that fails the rule before the response is built.

Triage

Every Dependabot alert below is already at or above its patched version on beta (next 16.3.4, sharp 0.35.4, fast-uri 3.1.7, hono 4.13.7, qs 6.16.0, js-yaml 4.3.2, vitest/@vitest/mocker 4.1.11 β€” in both package-lock.json and bun.lock), so no dependency bump is owed. They close when main receives the release.

Alert Package Sev Scope Does it reach this product? Verdict
#90, #97 next β€” RCE on windows-hosted servers critical runtime No. Linux only: Jetson arm64 plus the x64 Ubuntu installer; no Windows-hosted target exists Dismissed not_used
#91, #98 next β€” RCE in the Image Optimization API (AVIF) critical runtime Yes. No images.unoptimized in next.config.ts, and next/image is on the unauthenticated /login page. Measured on a box: /_next/image?url=… answers 200, and with an AVIF/WebP Accept it negotiates image/webp β€” the optimizer answers requests Fixed on beta (16.3.4)
#96 sharp β€” libvips/libheif high runtime Yes. serverExternalPackages names it and /setup-api/pets/thumb is traced with the libvips .so. Measured on a box: 200, image/png Fixed on beta (0.35.4)
#82, #85, #86, #87 fast-uri high runtime Not ruled out β€” reached through ajv, a runtime dependency of @modelcontextprotocol/sdk, and the MCP server runs on the box Fixed on beta (3.1.7)
#92, #93, #94 hono medium runtime Not on any live path: hono arrives only through @modelcontextprotocol/sdk's HTTP transport (@hono/node-server), and mcp/clawbox-mcp.ts instantiates StdioServerTransport and nothing else. Left open rather than dismissed β€” it is already fixed, and a future HTTP transport would make it live again Fixed on beta (4.13.7)
#83, #84 qs medium runtime Same: qs arrives through express/body-parser, again only the MCP SDK's HTTP transport, never instantiated. Left open for the same reason Fixed on beta (6.16.0)
#95 js-yaml high development Build host only (@eslint/eslintrc); nothing on a box runs eslint Fixed on beta (4.3.2)
#89, #88 vitest, @vitest/mocker medium development Build host only; the test runner never ships to a box Fixed on beta (4.1.11)

Code scanning, 4 open on refs/heads/main:

Alert Rule Location on main Verdict
#302 js/path-injection src/lib/code-projects.ts:591 Already fixed on beta β€” deployWebapp joins webappPath(appId), which rebuilds the id through safeProjectId. Closes on the release to main
#299 js/http-to-file-access src/lib/code-projects.ts:592 Same fix, same line
#300 js/remote-property-injection src/app/setup-api/preferences/route.ts:74 Fixed here
#301 js/remote-property-injection src/app/setup-api/preferences/route.ts:98 Fixed here

Secret scanning: 0 alerts, open or closed.

Dismissals made

Proof

RED, on unmodified beta:

vitest --project unit src/tests/routes/preferences.test.ts     5 failed | 24 passed (29)
vitest --project unit src/tests/unit/lockfiles-in-sync.test.ts 3 failed |  3 passed (6)

The five key-shape cases fail; the sixth β€” the runtime-assembled app_<appId>_settings name must still be stored β€” passes, which is the guard against over-tightening.

GREEN, on this branch:

vitest --project unit         958 files   16374 passed | 1 skipped (16375)
vitest --project components   252 files    2432 passed (2432)
tsc --noEmit -p .             exit 0
eslint (changed files only)   0 errors, 1 pre-existing warning
npm ci --dry-run              rc=0   (EUSAGE on beta)
bun install --frozen-lockfile --dry-run   rc=0
npm audit                     found 0 vulnerabilities
npm audit --omit=dev          found 0 vulnerabilities
CodeQL on refs/pull/921/merge results=0, 0 open alerts

For contrast, npm audit against main's lockfile: 9 vulnerabilities (5 moderate, 3 high, 1 critical).

On an OpenClaw box (web unit only β€” clawbox-setup restarted, the gateway never touched; branch head built with bun run build, BUILD_ID moved, commit stamped in build-info.json):

login page (anonymous)        200
desktop /                     200  (owner session)
/setup-api/setup/status       200
/_next/image?url=…&w=128      200  image/png 11945
/_next/image (Accept: avif)   200  image/webp
/setup-api/pets/thumb (sharp) 200  image/png 62044
/chat                         200
/setup-api/chat/capabilities  200
/setup-api/gateway            200

and the fix itself, against the live store:

POST {wp_opacity:80, "ui_"+300Γ—"a":"x"}   -> 400 {"error":"preference name is not one this box stores"}
POST {wp_opacity:80, "ui_a b":"x"}        -> 400 {"error":"preference name is not one this box stores"}
POST {ui_e2e_probe_unused:1, not_a_pref:"x"} -> 200 {"ok":true}   (the unowned name skipped, as always)
GET  ?keys=wp_opacity,<300-char name>     -> {}
junk names in config.json                 -> []

Both refusals were whole: the legal wp_opacity beside them did not land either, and the only key the run added was the one the third POST legitimately stored. The box was then put back on beta, rebuilt and restarted; config.json is byte-identical to before the test (same sha256, same 9 pref:* keys, still 600 clawbox:clawbox), the worktree is clean and both services are active.

Not run: a full chat turn. The chat transport is a gateway WebSocket and driving one would have written into the owner's own session; the chat page, its capabilities endpoint and the gateway proxy all answer 200 on the branch build, which is as far as this went.

One finding this PR does NOT fix

has_playwright_chromium (install.sh:1697, same shape in install-x64.sh) probes for the presence of any chrome-linux*/chrome under ~/.cache/ms-playwright, not for the revision the installed playwright-core pins β€” so ensure_playwright_chromium prints "already installed" and a playwright bump never pulls the Chromium it ships against (1.62.1 β†’ revision 1234, 1.63.0 β†’ revision 1243). playwright is a runtime dependency here, not just an e2e tool: setup-api/browser and the coding-run screenshot verification launch it.

Measured on a box on beta today: playwright-core 1.63.0, cache holds chromium-1243 and chromium_headless_shell-1243, browsers.json wants 1243 β€” matched, so the predicted skew is not present on the hardware available here. The probe is still a presence check rather than a revision check, and it belongs in its own PR with a device run of the update step; the bump that introduced the version gap was #832, not this branch.

What closes only after a release to main

All fifteen remaining Dependabot alerts and all four code-scanning alerts are computed against main. Merging this to beta closes none of them. They close when main receives the release that carries beta's dependency set and these fixes.

package.json asks for playwright and @playwright/test ^1.63.0 β€” bun.lock
carries 1.63.0, package-lock.json still pinned 1.62.1 and still declared
^1.62.1 in its root entry. The two lockfiles had drifted, and `npm ci`
refused the tree outright:

  npm error `npm ci` can only install packages when your package.json and
  npm error package-lock.json or npm-shrinkwrap.json are in sync.
  npm error Invalid: lock file's @playwright/test@1.62.1 does not satisfy
  npm error @playwright/test@1.63.0

CI and install.sh both build with `bun install --frozen-lockfile`, which is
why nothing caught it. It still matters: package-lock.json is the manifest
GitHub's dependency graph audits β€” fifteen of the seventeen open Dependabot
alerts are filed against it β€” so a copy that no longer matches package.json
makes those verdicts unreliable, and a contributor who reaches for npm
cannot install at all.

Regenerated with `npm install`; the diff is the playwright family and the
three declared ranges that had already moved in package.json. After it,
`npm ci --dry-run` and `bun install --frozen-lockfile --dry-run` both
succeed, and `npm audit` reports 0 vulnerabilities with and without dev.
The only rule on a preference NAME was the route's prefix test, so
everything after `ui_` was free β€” any length, any character. Both doors put
that caller-supplied name straight onto an object:

  result[key] = allConfig[`${PREFERENCE_KEY_PREFIX}${key}`];   // GET
  entries[`${PREFERENCE_KEY_PREFIX}${key}`] = value;           // POST

and the second one lands in config.json, so a caller with a session could
park unbounded arbitrary names in the owner's store β€” up to the 64 KB a
value may be, per name, with nothing bounding how many. Code scanning has
been flagging both writes (js/remote-property-injection, alerts #300 and
#301 on main); they are the two of the four open ones that beta had not
already closed.

`safePreferenceKey` is the rule, in the module that owns the preference
rules: a length bound and an alphabet, with the name REBUILT character by
character rather than tested and passed through β€” the same shape as
`safeProjectId` in code-projects.ts, and for the same reason, that a
`.test()` guard leaves the caller's own string in play at the sink. It also
refuses the three names every object literal already has, so a caller may
accumulate into a plain `{}` without that being load-bearing.

Applied at every door, not only the two the analyser named:
- the route's `isAllowed` becomes `allowedKey`, returning the rebuilt name,
  so both writes above are made of the alphabet. A name that does not
  survive is SKIPPED, exactly as a name with the wrong prefix already was β€”
  the alternative, a 400, would start refusing whole desktop writes over one
  stray entry.
- `validatePreference` checks the name before the value, which carries the
  rule to the read path (`sanitizePreferences`, both GET branches) so a name
  stored before this existed stops being served.
- `sanitizePreferenceValue` answers on the name before walking the value,
  which carries it to the machine door (`sanitizePreferenceWrites` β€” app
  install/uninstall, the webapp registry): those callers read entries back
  and write them out again, so a junk name they carried is dropped there
  rather than rewritten.

The MCP `preferences_set` tool needs nothing: it posts through this route
with a key bounded by its own enum.

128 characters is the bound, which leaves room for the one name this product
assembles at runtime β€” `app_<appId>_settings`, with a 64-character app id.
The suite pins that one, and the ten fixed names, against the rule.
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 18 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

The included review limit has been reached and this organization has disabled usage-based review continuation. Wait for reviews to reset or ask a billing admin to change After included review limits.

Learn how review limits work.

Review configuration:

βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: bfc222ee-5e86-4978-bbac-93d4ba5ed0f7

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3c589be and 4a913ee.

β›” Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
πŸ“’ Files selected for processing (10)
  • src/app/setup-api/ai-models/configure/route.ts
  • src/app/setup-api/apps/settings/route.ts
  • src/app/setup-api/apps/uninstall/route.ts
  • src/app/setup-api/preferences/route.ts
  • src/lib/preference-schema.ts
  • src/tests/routes/ai-models/configure.test.ts
  • src/tests/routes/apps/settings.test.ts
  • src/tests/routes/preferences.test.ts
  • src/tests/unit/lockfiles-in-sync.test.ts
  • src/tests/unit/preference-schema.test.ts

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

@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown

CI Summary

βœ… Tests

  • Result: passed
  • View run
  • Coverage: statements 87.73%, branches 80.98%, functions 87.69%, lines 90.36%

βœ… E2E

βœ… E2E Install

The drift the previous commit repaired has nothing standing in front of it.
Dependabot does not maintain package-lock.json β€” the npm ecosystem is
`open-pull-requests-limit: 0` β€” so a grouped bun bump moves package.json and
bun.lock and leaves this file where it was, and nothing in the repo installs
from it: CI and install.sh both build with `bun install --frozen-lockfile`,
and there is no `npm ci` anywhere under .github/. The next weekly bump would
have put it back out of sync with nobody the wiser until the dependency
graph started reporting a tree we do not ship.

The check is npm's own first test β€” `packages[""]` carries a copy of the
three dependency blocks and `npm ci` compares them β€” plus a range check on
the resolved top-level versions, which is the other half of what `npm ci`
refuses. Both run in the unit suite that already runs on every PR, so the
invariant costs no job.

RED against origin/beta's package-lock.json: 3 failed | 3 passed (6), the
three being the two dependency blocks and the resolved-range check.
Review of the previous commit, four things.

A name that failed the new rule was skipped and the response was still
`{ ok: true }` β€” the false-success shape, and it contradicted this route's
own header comment and preference-schema.ts's "the doors do not all answer a
bad value the same way" table, both of which say the user door rejects the
request whole. It matters because the desktop's writers branch on nothing but
the response: InstalledAppSettings renders "Saved" over `preferencesRes.ok`,
and usePreferenceWriter does not read the body at all.

The two cases are now told apart. A name this door does not own β€” one with no
preference prefix β€” is still skipped, because the caller was not asking this
route to store it. A name WITH the prefix but spelled impossibly is this
door's and is refused with a 400, like an impossible value, and nothing in
the request lands. The name is not quoted back: the body is both returned and
logged.

The 128-character bound was justified in a comment by a computation nothing
asserted β€” `app_` plus a 64-character app id plus `_settings` β€” while the only
boundary test derived its fixture from MAX_PREFERENCE_KEY_LENGTH itself, so it
would have passed at any value of it. Lowering the bound to 64 would have kept
every test green while an app with a long id silently lost its settings. The
widest assembled name is now a literal in the suite, with the constant
asserted against it.

Two comments corrected rather than left to mislead: the reason
`validatePreference` returns for a bad name is not surfaced by either caller
today, and the key in the rejected-write log line is no longer "only
prefix-checked" β€” `logSafe` there is now the second rule, not the only one.

And `sanitizePreferenceWrites` accumulates into a null-prototype object like
every other accumulator in that module. Its pass-through branch β€” the keys
that are not preferences at all β€” is the one assignment `safePreferenceKey`
does not stand in front of, so a config key named `__proto__` would have set
the object's prototype, `Object.keys` would have read it as empty and the
whole write would have been dropped in silence. No caller can send that name
today; all three build their updates from string literals.
@KrasimirKralev

Copy link
Copy Markdown
Contributor Author

CI green on db02f06de: test 14m16s, e2e 3m34s, e2e-install 23m15s, build-identity, review, CodeQL (Analyze (javascript-typescript)) all pass.

One note for the record: the test job failed once on this head before the rerun, on src/tests/components/chat-popup-dismiss-and-geometry.test.tsx > Escape inside the chat > closes the Create app card and leaves the conversation up β€” a 5.1 s timing case in a chat component this branch does not touch. It passed on the previous push of this same PR, passes locally (10/10 in that file, 2432/2432 in the components project), and passed on the rerun. Flake, not this diff.

CodeQL's verdict on the branch: refs/pull/921/merge analysed twice, once per push, results=0, 0 open alerts β€” so js/remote-property-injection #300 and #301 do not survive this change, and code-projects.ts is clean as well. CodeRabbit reports "Review rate limited" and has posted nothing to address.

beta has not moved since the branch was cut (3c589be71); git diff --stat origin/beta...HEAD lists only the six files this PR intends.

@KrasimirKralev

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…und the preference store

preferences: one write may name at most 32 keys β€” the read's own cap, the two
doors take the same names β€” and the store may hold at most 500 pref:* names,
which is what the KV route already bounds data/kv.json by. Both are refused 400
before anything is stored, and the second counts only NEW names so a box that
reached the cap can still change its wallpaper. The name was bounded in shape
by the previous commit and in nothing else: config.get() re-reads and re-parses
the whole file on every call, so a body of 5000 legal names took the desktop,
Settings and the wizard down with it. The accumulator is null-prototype like
every other one in this family, which is what the comment on safePreferenceKey
already claimed.

apps/settings: the writer table is a Map, so a lookup answers for own entries
only. As an object literal it answered for Object.prototype too β€” appId
"toString" resolved to a function that returns "[object Undefined]" without
throwing, and the route reported configWritten: true over a file it never
wrote, which the app renders as "Connected"; its siblings threw and leaked the
raw message with a 500. Every name Object.prototype carries is refused at the
door, and the ad-hoc charset rule is replaced by the shared APP_ID_RE, which
carries the 1-64 length the producers mint within β€” without it a caller could
park a megabyte-long skills.entries.<id> key in the harness's own config, and
an id the preferences route can no longer store app_<id>_settings under.

ai-models/configure: the same lookup one route over. PROVIDERS["toString"] is a
truthy function, so the "Unknown provider" guard under it never fired and the
handler carried on with a config that spreads to {}.

uninstall: the comment over the settings delete said setPreferences drops an
undefined. It does not β€” it would pass it through β€” so it says what is actually
true of that delete instead.
…sing entry

The resolved-tree half of the new guard could report green on a lockfile npm ci
refuses. Three holes, each with its own case now:

- a caret on a 0.x range stops at the next MINOR, not the next major. The
  manifest carries two (@xterm/addon-fit ^0.11.0, @xterm/addon-web-links
  ^0.12.0) and 0.12.0 was judged to satisfy ^0.11.0, which npm calls Invalid.
- a range shape the caret regex did not match was waved through. That was ^2
  and ^1 today (@noble/ed25519, @noble/hashes) and every future ~x.y.z, >=, *
  and npm: alias. Partial carets are read properly; anything else is an
  offender rather than a pass, so the day the manifest grows a shape this judge
  cannot read, the list says so by name.
- a dependency with no node_modules/<name> entry was skipped as "an optional
  dependency the tree does not carry" β€” but the skip ran for dependencies and
  devDependencies too, so a lockfile whose packages[""] was hand-edited to
  agree with package.json passed green where npm ci says "Missing: X from lock
  file". Only a declared optionalDependency is skipped now.

The scan is a function taking (pkg, lock), which is what lets those last two
cases be proved on synthetic input rather than on the repo's own files.
@KrasimirKralev

Copy link
Copy Markdown
Contributor Author

Delta review applied β€” two commits on top of db02f06de

Head is now 4a913ee78. origin/beta has not moved (3c589be71), so nothing was rebased.

MEDIUM

N1 β€” a preference name was bounded in shape and in nothing else. POST /setup-api/preferences now refuses 400 in two places, both before anything is stored:

  • MAX_KEYS_PER_WRITE = 32, counted on what the body names (before the prefix filter) and checked before the loop. The same number as MAX_KEYS_PER_READ and for the same reason β€” the two doors take the same names, so a body this route accepts should be one a single keys= query could ask back. The widest legitimate write is the desktop's appearance bundle, four names.
  • MAX_STORED_PREFERENCES = 500 β€” the KV route's MAX_ENTRIES, which bounds data/kv.json for exactly this reason β€” checked against config.getAll() immediately before setMany, and only for a body that actually stores something. The cap counts new names only: a box that reached it can still change its wallpaper.

config.get() re-reads and re-parses the whole file on every call, so the failure this closes is a body of 5000 legal names taking the desktop, Settings and the setup wizard with it.

N2 β€” the sibling door for the same app_<appId>_settings family. apps/settings:

  • CONFIG_WRITERS is a Map (the CLOSED_DOMAINS idiom from preference-schema.ts), so a lookup answers for own entries only. As an object literal it also answered for Object.prototype: appId: "toString" resolved to a function that returns "[object Undefined]" without throwing, and the route reported configWritten: true over a file it never wrote β€” which InstalledAppSettings renders as Connected. Its siblings (valueOf, hasOwnProperty, …) threw and leaked the raw message with a 500.
  • Every name Object.prototype carries is refused at the door (RESERVED_APP_IDS, taken from Object.getOwnPropertyNames(Object.prototype) rather than listed by hand, plus prototype), which replaces the three-name check.
  • The ad-hoc /^[A-Za-z0-9_-]+$/ is replaced by the shared APP_ID_RE, which carries the 1–64 length the producers mint within. Without it a caller could park a megabyte-long skills.entries.<id> key in the harness's own openclaw.json β€” and an id the preferences route can no longer store app_<id>_settings under.

Sibling sweep over src/app/setup-api/** and mcp/** for the same lookup shape and for ad-hoc app-id regexes:

  • ai-models/configure/route.ts:2240 PROVIDERS[provider] β€” the same defect, fixed here. provider is unvalidated caller input and PROVIDERS["toString"] is a truthy function, so the Unknown provider guard under it never fired and the handler carried on with a config that spreads to {}. Now Object.hasOwn; a regression test covers toString/valueOf/constructor/hasOwnProperty (RED: 500, GREEN: 400).
  • ai-models/catalog/route.ts:743 OFFERABLE_MODEL_ID_BY_PROVIDER[provider] β€” cleared: provider is closed-allowlisted by isCatalogProvider against CATALOG_PROVIDERS before any use, and both internal callers pass a provider from that list.
  • apps/install/route.ts:52 SLUG (no length bound) β€” cleared: safeAppId(slug) runs on the same value immediately after and is what leaves the door, rebuilt and bounded at 64.
  • apps/skill-info/route.ts:21, apps/store/route.ts:83, apps/icon/[appId], apps/uninstall, and the mcp/lib/schema.ts family (INSTALLED_APP_ID_BODY, zSlug) β€” cleared: all already length-bounded, several rebuilt rather than tested.
  • MIME_TYPES/CONTENT_TYPES in webapps, files/[...path], pets/sprite, chat/media β€” cleared: keys are dot-prefixed, and path.extname cannot produce a name Object.prototype carries.
  • Noted, not fixed: mcp/lib/web.ts:121 NAMED_ENTITIES[name] decodes &toString; from a fetched page into String(Object.prototype.toString). Cosmetic text corruption from a hostile page, no write, and it belongs with the scraper rather than here.

LOW / INFO

  • N3 β€” entries in the preferences POST is Object.create(null), so the blanket claim on safePreferenceKey ("every one of them is null-prototype") is now true rather than the new false comment L2 was raised for.
  • N4 β€” the lockfile guard's resolved-tree half could report green on a lockfile npm ci refuses. All three holes closed: ^0.x stops at the next minor (0.12.0 no longer satisfies ^0.11.0); partial carets (^2, ^1) are read properly and any other shape is an offender rather than a pass; a dependency with no node_modules/<name> entry is an offender unless the manifest declared it optional. The scan is now a function of (pkg, lock), which is what lets the last two be proved on synthetic input.
  • N5 β€” the comment at the write door named usePreferenceWriter and InstalledAppSettings together as if the 400 reached both. Only InstalledAppSettings checks res.ok; usePreferenceWriter still does .catch(() => {}). The comment says so.
  • L5 (first review) β€” the uninstall route's "setPreferences drops an undefined" was wrong (it would pass it through; setMany is where undefined means delete). Rewritten to say what is actually true of that delete.
  • N6 β€” skipped: .github/dependabot.yml is outside src/, and this pass kept the diff there.
  • L4 (first review) β€” skipped: the fifth copy of PREFERENCE_KEY_ALPHABET is a refactor across five modules and wants a PR of its own.
  • M1 (first review) β€” still deferred; install.sh is untouched by this PR and the 1.63.0/chromium skew is already live on beta through bun.lock. The manual step for a box that needs it: PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright npx playwright install chromium.
  • N7 β€” on record: codeql.yml analyses main and beta, so alerts raised on main do not close on a merge to beta; and the all=1 sink is deliberately unguarded because its key comes from the store, not the request β€” sanitizePreferences drops any stored name that fails.

Verification

RED first for every fix. npx vitest run --project unit src/tests/unit src/tests/routes β†’ 953 files, 16132 passed, 1 skipped. npx tsc --noEmit -p . clean. ESLint on the touched files: 0 errors, 2 pre-existing unused-var warnings in lines this pass did not touch. package-lock.json and bun.lock untouched by these two commits.

@KrasimirKralev
KrasimirKralev merged commit a68d427 into beta Sep 18, 2026
11 checks passed
@KrasimirKralev
KrasimirKralev deleted the chore/security-alerts-2026-09-18 branch September 18, 2026 13:18
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