security: M17 + L1 + L2 + L12 pre-launch follow-ups#58
Conversation
The original `thin_profile` heuristic only tripped when bio + email + followers were all weak simultaneously — a bot with two followers and a one-character bio walked straight through. Moderators only had a single coarse signal to triage with. Replace the lone signal with an additive bag persisted to `public.users.signup_flags`: `young_account` (created_at < 90 days), `low_repos` (< 3 public), `low_followers` and `low_following` (each < 2), `empty_bio` / `short_bio` (< 8 chars after trim), and `no_public_email`. The legacy `thin_profile` key stays so historical rows remain comparable. Closes the M17 follow-up from the 2026-06-01 audit (issue #49). None of these flags BLOCK login — the hard signup gate still lives in `callbacks.signIn`; flags here are soft moderator-triage signals only. Tests: 40-case table-driven coverage of legacy compatibility, every new signal, account-age edges (malformed/null/future-dated), bio thresholds, and combined real-world bot shapes.
L1 — postcss <8.5.10 (CVE-2026-41305, moderate 6.1) Add a pnpm override pinning every transitive postcss to ≥8.5.10. The CVE is an XSS via unescaped `</style>` in stringify output and isn't reachable from our code today (no user-supplied CSS), but pnpm audit flags it so leaving it in place forces a `pnpm audit --prod` exception. The override resolves every occurrence (next, tailwind, vite) to 8.5.15 without breaking the Next build (verified locally). L2 — uuid <11.1.1 (CVE-2026-41907, moderate 7.5) Document an audit-ignore via `pnpm.auditConfig.ignoreCves`. The buffer-overflow only triggers when v3/v5/v6 are called with a caller- supplied `buf` argument, which next-auth@4 never does (it calls v4() with no buffer). Patching would force a breaking next-auth bump (4 → 5); deferring until the planned next-auth v5 migration is cheaper and strictly safe given the call-site analysis. The deferral, justification, and revisit trigger live in `docs/security-audit-ignores.md`. After both: `pnpm audit --prod` reports "1 moderate (1 ignored)" with exit 0 — non-blocking but visible. Refs the L1 + L2 follow-ups from issue #49.
…(L12)
The E2E shim in `getSession()` previously short-circuited on a triple
gate: `E2E_TEST_AUTH_USER_ID` set, `x-e2e-auth: 1` header present,
`NODE_ENV !== 'production'`. Vercel sets `NODE_ENV=production` for
preview deploys so that triple gate was safe on Vercel, but a non-Vercel
preview (Render/Fly/local-over-a-tunnel) running `pnpm dev` and
inheriting `E2E_TEST_AUTH_USER_ID` by accident became a session-forgery
primitive: any caller setting the header could mint sessions for any
user-id.
Add two new gates:
- `ALLOW_E2E_AUTH === '1'` — explicit opt-in. Production / preview
deployments will never set this, so an ambient
`E2E_TEST_AUTH_USER_ID` leak alone can no longer enable the shim.
Playwright's `webServer.env` now sets it.
- `VERCEL_ENV !== 'production'` — belt-and-braces. Even if
`ALLOW_E2E_AUTH=1` somehow ships to a Vercel production deployment,
the shim still refuses.
Playwright config previously pinned `VERCEL_ENV=production` so e2e
tests exercised the production robots.txt branch. The new shim refuses
under that condition, so the pin is dropped. Production-mode robots
behaviour is already exhaustively covered by `tests/unit/robots.test.ts`;
`tests/e2e/seo.spec.ts` now smoke-tests the route returns a parseable
robots file in whichever branch the runtime picks.
Tests: `tests/unit/auth-e2e-shim.test.ts` covers the full gate matrix —
refuses when `ALLOW_E2E_AUTH` is unset/wrong-value, refuses on
`NODE_ENV=production`, refuses on `VERCEL_ENV=production` regardless of
the flag, allows on `VERCEL_ENV=preview`, refuses without the header.
Refs the L12 follow-up from issue #49.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Polish from the PR #58 self-review pass. No threat-model change; each hunk tightens an invariant that was technically safe but easy for a future contributor to break. - `lib/auth.ts`: replace `e2eShimEnabled()` (bool) + a deferred `process.env.E2E_TEST_AUTH_USER_ID as string` cast with `readE2EShimUserId()` (returns the id-or-null). The env read now happens BEFORE `await readE2EHeader()`, so a hypothetical concurrent handler that mutates the env mid-await cannot turn the synthetic session into `{ id: undefined }`. New test in `auth-e2e-shim.test.ts` simulates that race by mutating the env from inside the header-mock and asserting the captured id wins. - `lib/auth.ts`: add a NOTE comment over the synthetic-session return documenting that bypassing the per-request ban check is intentional AND only acceptable because the five gates collectively bar production. Reduces the chance a future contributor wires the branch to real user-id values and turns it into a banned-user impersonation primitive. - `lib/auth/soft-flag.ts`: add `clock_skew` signal for future-dated `createdAt` (either real skew or tampered payload — either way worth moderator attention). `young_account` no longer fires for skewed values; the two signals are mutually exclusive. - `lib/auth/soft-flag.ts`: `normalizeBio` now strips ZWSP/ZWNJ/ZWJ/BOM before measuring, so a bot can't dodge `empty_bio` (or pad past the `short_bio` threshold) with invisible whitespace. Tests: +8 cases across the two suites. Full unit suite 1146 ✓.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
package.json (1)
65-67: ⚡ Quick winBound the PostCSS override to major 8.
pnpm-lock.yamlcurrently resolvespostcssonly to8.5.15(nopostcssmajors > 8), but the override value">=8.5.10"can drift to PostCSS 9+ on future lockfile refreshes; prefer^8.5.10to keep major 8.Suggested change
"pnpm": { "overrides": { - "postcss@<8.5.10": ">=8.5.10" + "postcss@<8.5.10": "^8.5.10" },🤖 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 `@package.json` around lines 65 - 67, The override for PostCSS in package.json is too permissive ("postcss@<8.5.10": ">=8.5.10") and may allow PostCSS v9+ on future lockfile refreshes; change the override value to "^8.5.10" to pin the major to 8 while still allowing patch/minor updates, updating the entry that references "postcss@<8.5.10" so the RHS is "^8.5.10".
🤖 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.
Nitpick comments:
In `@package.json`:
- Around line 65-67: The override for PostCSS in package.json is too permissive
("postcss@<8.5.10": ">=8.5.10") and may allow PostCSS v9+ on future lockfile
refreshes; change the override value to "^8.5.10" to pin the major to 8 while
still allowing patch/minor updates, updating the entry that references
"postcss@<8.5.10" so the RHS is "^8.5.10".
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a92cb392-44c7-4c72-858d-148df6bc9d01
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
docs/security-audit-ignores.mdlib/auth.tslib/auth/soft-flag.tslib/github.tspackage.jsonplaywright.config.tstests/e2e/seo.spec.tstests/unit/auth-e2e-shim.test.tstests/unit/auth-org-sync.test.tstests/unit/auth/soft-flag.test.ts
Closes #49.
Four targeted fixes from the 2026-06-01 pre-launch audit. None are individually launch-blocking; bundled here so they don't fall out of memory before the June 7 milestone. Each finding is its own commit for review granularity.
M17 —
signup_flags.thin_profilestrengthenedlib/auth/soft-flag.tsBefore: the lone
thin_profilesignal only tripped when bio + email + followers were all weak simultaneously. A bot with 2 followers + a one-character bio walked through.After: additive signals persisted to
public.users.signup_flags:young_account— GitHub account < 90 days oldlow_repos— < 3 public reposlow_followers/low_following— each < 2empty_bio/short_bio— empty or < 8 chars after trimno_public_email— email unset on profilethin_profile— kept verbatim for historical-row compatibilityStill soft signals only — the hard signup gate (30d age + ≥1 public repo) is unchanged.
Tests: 40-case table-driven coverage including malformed/null/future-dated
created_at, bio thresholds, every signal in isolation, and combined real-world bot shapes.L1 — postcss <8.5.10 (CVE-2026-41305)
package.jsonAdded
pnpm.overridespinning every transitivepostcssto>=8.5.10. Lockfile regenerated. Next build still passes.Before:
After:
L2 — uuid <11.1.1 (CVE-2026-41907)
package.json,docs/security-audit-ignores.mdDocumented an audit-ignore via
pnpm.auditConfig.ignoreCves. The buffer-overflow only triggers whenv3()/v5()/v6()are called with a caller-suppliedbuf;next-auth@4only callsv4()with no buffer, so the vulnerable code paths are unreachable. Patching would force a breaking 8 → 11 major bump undernext-authand is deferred until the plannednext-auth@5migration. Full justification and revisit trigger live in the new doc.L12 — E2E auth shim hardened
lib/auth.ts,playwright.config.ts,tests/e2e/seo.spec.ts,tests/unit/auth-e2e-shim.test.tsBefore: triple-gated (
E2E_TEST_AUTH_USER_IDset +x-e2e-auth: 1header +NODE_ENV !== 'production'). On a non-Vercel preview runningpnpm devthat inheritedE2E_TEST_AUTH_USER_IDby accident, the header alone could mint sessions for any user-id.After: two additional gates:
ALLOW_E2E_AUTH === '1'— explicit opt-in flag. An ambient env-var leak alone can no longer enable the shim. Playwright'swebServer.envsets it.VERCEL_ENV !== 'production'— belt-and-braces refusal on Vercel production even if the flag somehow shipped.Playwright previously pinned
VERCEL_ENV=productionto exercise the prod robots.txt branch in e2e. That pin is dropped (it now collides with the L12 reject). Prod robots behaviour is already fully covered intests/unit/robots.test.ts;tests/e2e/seo.spec.tsnow smoke-tests the route in whichever branch the runtime picks.Tests:
tests/unit/auth-e2e-shim.test.tscovers the full gate matrix — refuses withoutALLOW_E2E_AUTH, refuses on productionNODE_ENV, refuses on productionVERCEL_ENVregardless of the flag, allows onVERCEL_ENV=preview, refuses without header.Audit output diff
Before: 2 findings (postcss, uuid).
After:
The remaining moderate is the documented L2 uuid finding, marked ignored — non-blocking but visible.
Test plan
pnpm audit --prodexits 0 with1 moderate (1 ignored)ALLOW_E2E_AUTHnot set)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests
Chores