GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |
GeezerVison is showing github.com — fix(server): sweep expired sessions and cap the default memory store by JosunLP · Pull Request #267 · bQuery/bQuery Conversation
Collaborator
Summary
MotivationFixes #256. In a long-running single-instance deployment, which is the case this store is documented for, memory grew with every distinct visitor that ever received a session. Changes included
Validation
Checklist
Notes for reviewers
🤖 Generated with Claude Code https://claude.ai/code/session_01MdbZH1BQLrEGMoMD3gdBBo Generated by Claude Code
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactionsmemoryStore() only dropped an expired entry when that same id was read again, so sessions of clients that never returned stayed in memory forever, and session()'s default store had no size limit. - sweep expired entries during writes (at most every 30 s, and always before evicting for maxEntries so a live session is not dropped while dead ones remain) - touch() re-inserts, so maxEntries evicts the least recently used entry - session()'s default store is capped at 10 000 entries Fixes #256 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MdbZH1BQLrEGMoMD3gdBBo
|
There was an error while loading. Please reload this page.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
…thing can expire get() did not count as use, so the capped default store could evict a session that is only being read (rolling off). And once the store was at its cap every new session triggered a full sweep; track the earliest possible expiry and skip the walk before it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MdbZH1BQLrEGMoMD3gdBBo
## Summary
- In **signed** mode (`csrf({ secret })`), if `session()` runs before
`csrf()`, the CSRF secret is now stored in the server-side session
instead of a cookie (synchronizer-token pattern).
- Tokens are bound to the session **and to its id**: `$regenerate()`
(call it on login) invalidates the token, so pages rendered after login
get a fresh one.
- No CSRF cookie is set in this mode.
- The secret is written to the session only when `csrfToken(ctx)` is
called, so requests that never render a form do not create sessions.
- New option `bindToSession: false` restores the previous cookie-based
behaviour. Without a session, and in unsigned mode, nothing changes.
## Motivation
Until now, the signed token was `cookieSecret.HMAC(cookieSecret)`, and
verification only checked that the token matched the cookie. Nothing
tied that pair to a user. Anyone who can set a cookie for the victim can
therefore obtain a validly signed pair for themselves, plant its secret
as the victim's cookie, and forge requests. Setting a cookie for the
victim is possible from a sibling subdomain, or by a network attacker
for a cookie without the `__Host-` prefix.
The docs said signing defends against exactly this, which was wrong.
This is security-relevant, which is why it was reported privately rather
than as a public issue.
## Changes included
- `src/server/csrf.ts`:
- The session-bound path. The secret is stored as `{ secret, sid }`, and
a mismatch with `session.$id` counts as missing. This closes session
fixation: `$regenerate()` keeps the session payload, so an attacker who
planted a session cookie would otherwise already know the CSRF token of
the session the victim logs into.
- A lazy token provider in `ctx.state`; `csrfToken()` resolves it.
- The new `bindToSession` option with JSDoc.
- Corrected module docs.
- `docs/guide/server.md`:
- Explains the cookie-injection limit, the session binding and the
rotation on login.
- Documents the per-visitor session cost and the race between two
concurrent first-visit requests.
- Recommends `__Host-bq.csrf` for apps without sessions.
- `tests/server-stable.test.ts`:
- no CSRF cookie in session mode;
- a token minted for another session is rejected, i.e. the injection
case;
- no session is created without `csrfToken()`;
- the token rotates on `$regenerate()`: the pre-login token is rejected
and a freshly rendered one works;
- a POST without a minted token is rejected;
- the `bindToSession: false` opt-out.
## Validation
- [x] `bunx eslint .` + `bun run format:check`
- [x] `bun run test:types` + `bun run lint:types`
- [x] `bun run check`
- [x] `bun test`: 3524 pass, 0 fail
## Checklist
- [x] I followed the contributing guidelines.
- [x] I added or updated tests where behavior changed.
- [x] I updated documentation when public behavior or developer workflow
changed.
- [x] I confirmed the change does not introduce security-sensitive
regressions.
## Notes for reviewers
- **Behaviour change (signed mode + `session()` only):**
- No `bq.csrf` cookie is issued any more.
- The token is a random value stored in the session.
- The token changes when the session id is regenerated, so apps must
render a new token after login.
- Clients that read the cookie directly need `bindToSession: false`.
- **Merge together with #267:** pages that render a token create a
session per cookieless visitor. #267 caps the default `memoryStore()`
and sweeps expired entries; the docs here assume that bound.
- Known limitation (documented): two requests racing on the very first
visit each create a session and only the last cookie wins. Fixing that
needs merge-on-save in the session middleware, which is out of scope
here.
- Consider a GitHub Security Advisory for ≤ 1.17.0.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01MdbZH1BQLrEGMoMD3gdBBo
---------
Co-authored-by: Claude <[email protected]>
There was an error while loading. Please reload this page.
## Summary - Release PR for **1.17.1**. It brings everything on `dev` since `1.17.0` into `main`. - Security-and-correctness patch closing the 1.17.0 audit: - three security fixes in `server` and `ssr`; - ten bug fixes across `core`, `router`, `server`, `reactive`, `forms`, `store`, `component` and `view`. - No API removals and no module status transitions. A few behaviour changes are listed under **Notes for reviewers**. ## Motivation `dev` is 13 PR merges ahead of `main` (#259–#271). Together they close issues #246–#253, #256 and #258 (all closed) and the three privately reported vulnerabilities fixed in #269, #270 and #271. ## Changes included This PR targets `main`, so the diff shows the whole `1.17.0 → 1.17.1` delta: **48 files, +2007/−100**. It has three commits on top of `dev`. **Release commit (`d9d20de`)** contains only metadata and documentation: - **Version:** `package.json` → `1.17.1`. `AGENT.md`, `llms.txt`, `.github/copilot-instructions.md`, `.cursorrules` and `.clinerules` are synced; `check:ai-guidance` passes. `SECURITY.md` already lists `1.17.x`. - **Changelog:** `CHANGELOG.md` gets a `[1.17.1] - 2026-09-28` entry with Security / Fixed / Changed sections. - **Release notes:** new `docs/release-notes/1.17.1.md`, plus a row in the release-notes index. - **Pre-existing doc fixes:** - The 1.17.0 release notes page was missing from the sidebar. - Its date and CHANGELOG anchor said `2026-09-21`; the release date is `2026-09-25`, so the anchor pointed nowhere. - **Guides aligned with shipped behaviour:** - `api-core.md` describes `show()`/`hide()`/`toggle()` and camelCase `css()` as they work after #262/#263. - `router.md` documents decoded params (#260). **Review-fix commits (`61198bc`, `640830a`)** address the findings from the review on this PR: - **Session-bound CSRF (#271):** - Requests without a stored session use the signed CSRF cookie and never write to the session. Anonymous traffic can no longer fill the capped session store or evict real sessions from it. - A session created during such a request adopts the cookie secret, so forms rendered earlier still verify. - A session created or regenerated with `$regenerate()` (a login) always gets a fresh secret, so a planted cookie cannot reach the authenticated session. - A session removed with `$destroy()` is not written back. - For a stored session the secret is bound before `next()`, so a token read later from a streamed body or `ctx.sse()` stays valid. - **`toggle()` (#263):** `hidden="until-found"` counts as hidden even with an inline `display`, because it hides through `content-visibility`. The `api-core.md` list entry now names the inline-`display` exception. Source changes by PR: | PR | Change | | --- | --- | | #269 | Security: `serveStatic` `realpath` containment for precompressed sidecars | | #270 | Security: Node adapter answers handler errors with `500` instead of crashing; cancels streams and aborts `request.signal` on disconnect | | #271 | Security: signed CSRF tokens bound to the session and rotated on `$regenerate()` (refined in `61198bc`/`640830a`) | | #259 | `deepEqual`/`isEqual` kind mismatches (#246) | | #260 | Router: decoded params, encoded static segments (#247) | | #261 | Server: encoded static route segments (#248) | | #262 | `css()` camelCase (#249) | | #263 | `toggle()`/`hide()`/`show()` (#250) | | #264 | Already-aborted `AbortSignal` in `http` and `listen()` (#251) | | #265 | Linear-time `email()` (#252) | | #266 | Trusted Types for component render, `createTemplate()` and the DOM sanitizer (#253) | | #267 | `memoryStore()` sweeping, LRU eviction, default cap (#256) | | #268 | `SECURITY.md` supported line and drift check (#258) | ## Validation - [x] `bunx eslint .` + `bun run format:check` - [x] `bun run build`, `check:publish`, `check:size`, `check:package` - [x] `bun test`: 3568 pass, 0 fail; `test:types` and `lint:types` clean - [x] `bun run test:coverage` (coverage floor), `bun run check` (incl. `check:ai-guidance` → 1.17.1) and `bun run docs:build` (no dead links) - [x] Each regression test added in the review-fix commits fails without its fix. ## Checklist - [x] I followed the contributing guidelines. - [x] I added or updated tests where behavior changed. They were added in the individual fix PRs and in the review-fix commits. - [x] I updated documentation when public behavior or developer workflow changed. - [x] I confirmed the change does not introduce security-sensitive regressions. ## Notes for reviewers **Behaviour changes when upgrading** (also in the CHANGELOG and release notes): - **Router params are decoded.** Remove any manual `decodeURIComponent()`. - **CSRF with `session()` before `csrf({ secret })`:** - requests with a stored session get no `bq.csrf` cookie; the secret lives in the session; - visitors without a session keep the signed `bq.csrf` cookie; - the token rotates on `$regenerate()`, so render a fresh `csrfToken(ctx)` after login; - `bindToSession: false` opts out. - **`listen({ signal })`** rejects when the signal is aborted before or during binding. - **The default session store** evicts beyond 10 000 live sessions. - **`show()` without an argument** keeps the inline `display` of an element that is not inline-hidden. - **`email()`** rejects empty domain labels and addresses over 254 characters. **Known limitation:** forms rendered to visitors without a session (for example the login form) rely on the signed cookie, as before 1.17.1. The server guide recommends a `__Host-` cookie name for that case. **After merging:** tag `v1.17.1`, publish, and merge `main` back into `dev`, as after 1.17.0. The review-fix commits exist only on this branch, so that merge back is required. Consider publishing a GitHub Security Advisory for ≤ 1.17.0 that covers #269–#271. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MdbZH1BQLrEGMoMD3gdBBo --- _Generated by [Claude Code](https://claude.ai/code/session_01MdbZH1BQLrEGMoMD3gdBBo)_ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved protection against static-file sidecar paths escaping the configured root and strengthened CSRF protection for session-backed requests. * Node server handlers now handle errors and client disconnects more safely; server startup also responds correctly to cancelled requests. * Fixed URL-encoded route matching, CSS property handling, visibility toggling, equality checks, HTTP cancellation, and email validation. * Expired sessions are cleaned up, and the default in-memory session store now limits its capacity. * **Documentation** * Added version 1.17.1 release notes and updated security guidance and feature documentation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <[email protected]>
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.