fix(core): report a hand-rolled in-memory store under $lib/server - #355
Conversation
Closes #354. `security/handler-state-write` exempts `.set()`/`.update()` on imports resolving under the `$lib` server root — that is where database and KV clients live, and `db.set(…)` on one is persistence, not shared state. The check was purely path-based, so a plain `new Map()` in the same directory was exempt too, which is exactly the shape that serves one user's data to the next. The call shape cannot separate the two, and the pure parse cannot read another file, so arbitration moves to the collector, which has the Runtime: `parseKitModuleFacts` records the deferred write with the resolved path and the exported name, and `collectKitModuleFacts` reads the target module and promotes only the writes whose export is an in-memory container. Precision-first, like the rest of this default-on rule. An export initialized to `new Map`/`Set`/`WeakMap`/`WeakSet` or to an object/array literal is reported; anything else — a client built from a package import, a re-export, a module that cannot be found or read — stays exempt, so what the read cannot positively identify is silence rather than a false positive. Only the modules a handler actually writes to are read, asserted by a test, so a project whose handlers never touch `$lib/server` does no extra I/O. Aliased imports resolve through the exported name. Property writes were already reported everywhere and are untouched. Both the CLI and the Vite plugin go through the same collector, so both gain this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe rule defers ChangesServer store arbitration
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Handler
participant Parser
participant Collector
participant ServerModule
participant Rule
Handler->>Parser: parse .set()/.update() call
Parser->>Collector: return pendingServerStoreWrites
Collector->>ServerModule: resolve and read target module
ServerModule-->>Collector: return exported bindings
Collector->>Rule: promote in-memory container writes
Rule-->>Handler: report imported state write
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
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 @.changeset/server-store-arbitration.md:
- Around line 26-27: Update the sentence in the changeset so it clearly states
that an unreadable module remains exempt because an unseen wrapper is treated as
silence rather than a false positive; correct the grammatical error without
changing the documented conservative exemption behavior.
In `@packages/core/src/kit-module-collect.ts`:
- Around line 66-85: Update inMemoryExportsOf and MODULE_CANDIDATES so explicit
.js repoPath values are checked exactly first, then resolved with the .js suffix
removed using .ts, .js, and index forms, avoiding candidates such as
store.js.ts. Preserve existing resolution for paths without .js, and add
coverage for the NodeNext-style import mapping to the TypeScript source.
In `@packages/core/test/kit-module-collect.test.ts`:
- Around line 53-60: Rename the test describing the persistence-client exemption
to state the verified behavior only, removing the explanatory “reason the
exemption exists” clause; leave the test implementation unchanged.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 774ccec9-c4f3-49a2-976e-e5704a4e4329
📒 Files selected for processing (9)
.changeset/server-store-arbitration.mddocs/src/content/docs/ja/rules/security/handler-state-write.mddocs/src/content/docs/rules/security/handler-state-write.mdpackages/core/src/kit-module-collect.tspackages/core/src/kit-module-parse.tspackages/core/src/kit-module.tspackages/core/test/correctness-rules.test.tspackages/core/test/kit-module-collect.test.tspackages/core/test/security-kit-rules.test.ts
The candidate list appended extensions unconditionally, so an import written `from '$lib/server/store.js'` — how a NodeNext/ESM TypeScript project spells an import of its own `.ts` source — produced `store.js.ts` and `store.js.js`, matched nothing, and left the write unarbitrated. Verified as a real miss against a fixture before fixing. A path already carrying an extension is now checked as written, and a `.js` one is then remapped to `.ts`. Extensionless paths are unchanged. A client imported the same way still resolves and stays exempt. Also from review: a test was named after why it exists rather than what it verifies, which is the AGENTS.md rule added in #347, and the changeset had an ungrammatical sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All three applied. The
A path that already carries an extension is now checked as written first, then a Worth noting this is the failure mode the arbitration is designed around: an unresolvable target means unarbitrated, which means exempt — so the bug was a miss rather than noise. That is the right direction to fail, but it also means this class of gap does not announce itself, which is why the resolution list deserves the test it now has. The test name was named after why it exists rather than what it verifies — the AGENTS.md rule I added in #347, broken in the same session. The changeset sentence was ungrammatical; reworded.
|
Closes #354.
The gap
security/handler-state-writeexempts.set()/.update()on imports resolving under the$libserver root. That exemption earns its keep — it is where database and KV clients live, anddb.set(…)on one is persistence, not shared state. But the check was purely path-based, so this was exempt too:One shared instance, overwritten by every request, readable by every other — the exact failure the rule exists to catch, hidden by the directory it sits in. The sibling rule cannot cover it either:
collectKitModuleFactsdeliberately does not scansrc/lib/server/**, sosecurity/server-module-statenever sees the declaration.Approach
The call shape cannot separate a hand-rolled store from a client, and
parseKitModuleFactsis pure — it cannot read another file. So arbitration moves one level out, to the collector that already holds theRuntime:parseKitModuleFactsstops silently dropping the call. It records apendingServerStoreWritesentry carrying the resolved path and the exported name (so an aliasedimport { db as store }still resolves).collectKitModuleFactsreads each targeted module once and promotes intoimportedStateWritesonly the writes whose export is an in-memory container.A consumer that ignores the new field sees exactly the old behaviour, so nothing changes for anything downstream that has not opted in.
Precision-first, like the rest of the rule
Reported: an export initialized to
new Map/Set/WeakMap/WeakSet, or to an object or array literal.Exempt: everything else — a client constructed from a package import, a re-export, a module that cannot be found or read. What the read cannot positively identify as a container stays silent. In a default-on security rule a missed finding is the cheaper failure than noise on every project using Drizzle.
Verified
Three cases, run against the built CLI on a real fixture:
new Map()under$lib/server,.set()from a handlerdrizzle(url)under$lib/server,.set()from a handlerPlus six collector tests covering the index-form module path, the unreadable/missing target, and — asserted explicitly — that only the modules a handler actually writes to are read, so a project whose handlers never touch
$lib/serverdoes no extra I/O.Property writes (
store.user = …) were already reported wherever they appear and are untouched. Both the CLI and the Vite plugin go through this collector, so both surfaces gain it.Docs
The exemption paragraph on the rule page now states the real condition and shows the two-line contrast, en and ja together. Changeset is a
@svelte-vitals/coreminor.build,typecheck,test(1149 in core),lint,check:publish,smoke,blume checkand the docs build all pass.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Map,Set, weak collections, objects, and arrays.Documentation