fix: codebase analysis findings 2026-05-23 - #46
Conversation
Security: - Fix broken actions/checkout@v6 tag → @v4 (workflow failed at checkout) - Add authMiddleware to POST /logout (belt-and-suspenders CSRF protection) - Add Strict-Transport-Security and Referrer-Policy security headers - Add depth guard (max 100) to archiveRecursive to prevent stack overflow Deps (CVE fixes): - hono 4.12.8 → 4.12.22 (GHSA-qp7p-654g-cw7p and 10+ other advisories) - vite 6.4.1 → 6.4.2 (GHSA-p9ff-h696-f583 arbitrary file read, GHSA-4w7w-66w2-5vf9 path traversal) - postcss 8.5.8 → 8.5.15 (GHSA-qx2v-qp2m-jg93 XSS via unescaped </style>) - turbo 2.8.19 → 2.9.14 (GHSA-hcf7-66rw-9f5r CSRF/session fixation, GHSA-3qcw-2rhx-2726 local code execution) Quality / bugs: - updatePageSchema.title: add .min(1) to reject empty-string PATCH - Remove dead ReorderPagesInput type export (zero imports in repo) - Extract AUTOSAVE_DEBOUNCE_MS = 1000 constant in Editor.tsx - Add composite index idx_pages_sort_order(created_by, archived_at, parent_page_id, sort_order) for GET /pages ORDER BY performance - Finalize session cleanup prepared statement on startup - Align packageManager field to bun@1.3.11 (matches CI setup-bun version) - Add *.pem *.key *.p12 *.p8 to .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Warning Review limit reached
Your plan currently allows 1 review/hour. Refill in 21 minutes and 49 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (10)
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 |
There was a problem hiding this comment.
Sintesi
PR ben strutturata con fix concreti. Trovati 4 problemi da correggere prima del merge.
Findings
[MEDIUM] 1. Off-by-one nel depth guard — pages.ts:331
Con MAX_ARCHIVE_DEPTH = 100, la condizione depth > 100 e falsa a depth=100 — il 101-esimo livello dalla radice viene processato. Se il nome implica max 100 livelli (profondita 0-99), cambiare in depth >= MAX_ARCHIVE_DEPTH.
[MEDIUM] 2. Version constraint hono troppo larga per CVE — server/package.json:18
^4.12.18 ammette versioni da 4.12.18 a 4.12.21 potenzialmente ancora vulnerabili (GHSA-qp7p-654g-cw7p). Il lockfile risolve a 4.12.22, ma una fresh install senza lockfile potrebbe portare una versione pre-fix. Cambiare in ^4.12.22.
[MEDIUM] 3. Version constraint postcss troppo larga per CVE — web/package.json:45
Analogo: ^8.5.10 ammette versioni fino a 8.5.14 vulnerabili (GHSA-qx2v-qp2m-jg93). Lockfile risolve a 8.5.15. Cambiare in ^8.5.15.
[LOW] 4. authMiddleware su /logout — regressione UX — auth.ts:101
Con authMiddleware, un utente con sessione gia scaduta riceve 401 prima del handler — il cookie non viene cancellato. Se il frontend gestisce 401 su /logout con clear del cookie, il rischio e nullo; altrimenti verificare prima del merge.
| }) | ||
|
|
||
| .post("/logout", async (c) => { | ||
| .post("/logout", authMiddleware, async (c) => { |
There was a problem hiding this comment.
Potenziale regressione UX. Con authMiddleware, un utente la cui sessione e gia scaduta o invalidata server-side riceve 401 prima che il handler venga eseguito — il cookie non viene cancellato. Lx utente non riesce a fare logout finche il cookie non scade naturalmente.
Verificare che il frontend gestisca esplicitamente 401 su /logout (clear del cookie e dello stato locale) prima del merge.
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Sintesi
PR risolve CVE multipli (hono, vite, postcss, turbo), aggiunge authMiddleware su logout, depth guard su archiveRecursive, fix validazione title. Due problemi bloccanti:
- CI failing —
authMiddlewaresu logout rompe il testPOST /api/auth/logout > returns ok without cookie present(atteso 200, ricevuto 401). Fix: aggiornareauth.test.ts:172per aspettarsi 401. - CVE hono incompleto —
bun.lockrisolvehono@4.12.18mapackage.jsonrichiede^4.12.22(>=4.12.22). Eseguirebun installe committare il lockfile aggiornato.
Findings
| Nr | Severita | File | Issue |
|---|---|---|---|
| 1 | CRITICAL | apps/server/src/routes/auth.ts:101 |
Test regression logout — CI in errore |
| 2 | HIGH | bun.lock:783 |
hono risolto a 4.12.18, non soddisfa ^4.12.22 |
| 3 | MEDIUM | apps/server/src/routes/pages.ts:332 |
Errore depth guard propagato come 500 con messaggio esposto |
| }) | ||
|
|
||
| .post("/logout", async (c) => { | ||
| .post("/logout", authMiddleware, async (c) => { |
There was a problem hiding this comment.
[CRITICAL - CI failing] Aggiungere authMiddleware rompe il test esistente:
(fail) POST /api/auth/logout > returns ok without cookie present
Expected: 200
Received: 401
at apps/server/src/routes/auth.test.ts:172
La modifica di sicurezza e corretta; il test va aggiornato. In auth.test.ts:172:
- expect(res.status).toBe(200);
+ expect(res.status).toBe(401);Considerare di rinominare il test in "returns 401 without valid session".
| "headers-polyfill": ["headers-polyfill@5.0.1", "", { "dependencies": { "@types/set-cookie-parser": "^2.4.10", "set-cookie-parser": "^3.0.1" } }, "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA=="], | ||
|
|
||
| "hono": ["hono@4.12.8", "", {}, "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A=="], | ||
| "hono": ["hono@4.12.22", "", {}, "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw=="], |
There was a problem hiding this comment.
[LOW - bun.lock workspace specifier mismatch] La versione risolta nel lockfile e corretta (hono@4.12.22, CVE patchato). Tuttavia la sezione workspace di bun.lock registra il specifier come ^4.12.18 mentre apps/server/package.json richiede ^4.12.22. Questo mismatch causa il fallimento di bun install --frozen-lockfile. Ri-eseguire bun install per allineare il lockfile.
| const archiveRecursive = (pageId: string) => { | ||
| const archiveRecursive = (pageId: string, depth = 0) => { | ||
| if (depth >= MAX_ARCHIVE_DEPTH) { | ||
| throw new Error(`Page tree exceeds maximum depth of ${MAX_ARCHIVE_DEPTH}`); |
There was a problem hiding this comment.
[MEDIUM - error message exposure] Questa eccezione viene gettata dentro db.transaction() e non e catturata nel route handler. Hono restituisce un 500 con il messaggio interno "Page tree exceeds maximum depth of 100" visibile al client.
Catturare nel handler e restituire un 422: wrappare db.transaction(...) in try/catch, rilevare questo errore via e instanceof Error && e.message.startsWith("Page tree exceeds"), poi rispondere con c.json(...) e status 422. Alternativamente definire una classe ArchiveDepthError per un instanceof piu preciso.
PR added authMiddleware to POST /api/auth/logout, but logout must stay reachable without a valid session: a client whose cookie has already expired still needs the server to clear it and return ok. Test 'returns ok without cookie present' was broken by this. The handler already no-ops when no token is found (no DB write, nothing to leak), so dropping the middleware is safe. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves bun.lock conflict by re-running `bun install` against the merged dependency tree from main. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
actions/checkout@v6tag →@v4(workflow failing at checkout step — v6 does not exist)authMiddlewaretoPOST /logout(belt-and-suspenders CSRF protection; cookie isSameSite=Strictbut same-site top-level navigations bypass it)Strict-Transport-SecurityandReferrer-Policyheaders (additive, no regression risk)archiveRecursive— unbounded recursion crashed server inside live DB transaction on deep/cyclic trees</style>in CSS)updatePageSchema.titlelacked.min(1)—PATCH /api/pages/:id { "title": "" }silently stored empty title in DBReorderPagesInputtype export (zero imports across entire repo)AUTOSAVE_DEBOUNCE_MS = 1000constant inEditor.tsx(magic number)idx_pages_sort_order(created_by, archived_at, parent_page_id, sort_order)—GET /pagesORDER BY was doing a filesort after the existing index scanpackageManagerfield tobun@1.3.11(CI uses 1.3.11, field said 1.3.10)*.pem *.key *.p12 *.p8to.gitignoreFindings (deferred — implementabili in run dedicata)
Deferred — File upload magic-byte validation
File:
apps/server/src/routes/pages.ts:220-278Severità: MEDIUM
Soluzione proposta: Install
file-typenpm package; read first bytes of uploaded file and compare magic bytes to allowed image types before saving. Client-suppliedContent-Typeand file extension are fully attacker-controlled.Patch indicativa:
Decision points: Which MIME types to allow; whether to re-encode images server-side (eliminates embedded payloads).
Trade-off:
file-typeis a new dep; without re-encoding, a valid JPEG container could embed a malicious payload for exploits that parse JPEG metadata (rare but possible).Test richiesto: Upload a
.jpgfile withContent-Type: image/jpegthat is actually a JS file; expect 422.Deferred — Authorization bypass via ancestor-cycle (PATCH /pages/:id parentPageId)
File:
apps/server/src/routes/pages.ts:199-201Severità: HIGH
Soluzione proposta: Walk ancestor chain from
input.parentPageIdup to root before accepting reparent; reject with 400 if the walk reachesid.Patch indicativa:
Decision points: Whether to return 400 (bad request) or 422 (unprocessable); whether to expose the reason to the client.
Trade-off: Adds O(depth) DB reads per reparent call. For typical sidebar depth (≤10) this is negligible.
Test richiesto: Create page A → B → C; PATCH A with
parentPageId: C; expect 400.Deferred — sortOrder race condition (POST /pages and POST /databases)
File:
apps/server/src/routes/pages.ts:113,apps/server/src/routes/databases.ts:104-135Severità: HIGH
Soluzione proposta: Move
getNextSortOrder()call inside thedb.transaction()block so the read and write are atomic.Patch indicativa:
Decision points: None — pure correctness fix. SQLite WAL serialises writes so no data corruption occurs, but concurrent creates get duplicate sortOrder values and ordering becomes non-deterministic.
Trade-off: Low risk. No API change.
Test richiesto: Two concurrent POST /pages requests with same parentPageId; assert resulting sortOrder values are distinct.
Deferred — Cell upsert lacks composite primary key (PUT /databases/:id/rows/:rowId/cells/:propId)
File:
apps/server/src/routes/databases.ts:503-525,apps/server/src/db/schema.ts:79-87Severità: MEDIUM
Soluzione proposta: Add composite PK
(row_id, property_id)todatabase_cell_valuesin schema + new Drizzle migration; replace DELETE+INSERT withonConflictDoUpdate.Patch indicativa:
Decision points: Migration must include
CREATE UNIQUE INDEXon existing data with no duplicates — verify first.Trade-off: Schema migration on live table. Run
SELECT row_id, property_id, COUNT(*) FROM database_cell_values GROUP BY 1,2 HAVING COUNT(*) > 1to confirm no duplicates before applying.Test richiesto: Two concurrent PUT cell requests for same (rowId, propertyId); assert only one row exists after both complete.
Deferred — CORS allows all non-browser origins unconditionally
File:
apps/server/src/index.ts:27-28Severità: MEDIUM
Soluzione proposta: Return
null(deny) instead of the falsy origin to block server-side relay with credentials.Patch indicativa:
Decision points: Will this break any server-side clients (cron, health check, internal proxy) that call the API with credentials? List all internal callers before applying.
Trade-off: If the API is also used by non-browser clients that rely on CORS credentials passthrough, those clients will break.
Test richiesto:
curl -H 'Cookie: session=...' http://localhost:3000/api/auth/meshould return 401/CORS rejection after fix.Deferred — N+1 queries in archiveRecursive (replace with recursive CTE)
File:
apps/server/src/routes/pages.ts:328-349Severità: HIGH (performance)
Soluzione proposta: Replace recursive function with a single Drizzle SQL recursive CTE that archives all descendants in one query.
Patch indicativa:
Decision points: Verify bun-sqlite supports recursive CTEs (SQLite 3.8.3+; bun ships with SQLite 3.46+ — safe).
Trade-off: More complex SQL, harder to unit-test in isolation. The depth guard added in this PR mitigates the immediate crash risk.
Test richiesto: Create 50-page deep tree; archive root; verify all 50 descendants archived in one DB round-trip (use SQLite EXPLAIN QUERY PLAN).
Deferred — N+1 UPDATE loops in reorder endpoints
File:
apps/server/src/routes/pages.ts:167-174,apps/server/src/routes/databases.ts:344-356Severità: HIGH (performance)
Soluzione proposta: Replace per-item UPDATE loop with a single bulk CASE-expression UPDATE.
Patch indicativa:
Decision points: Drizzle raw SQL syntax may need adjustment for bun-sqlite driver. Test with 3+ items.
Trade-off: More complex SQL, but eliminates 100x network round-trips for large sidebars.
Test richiesto: Reorder 10 pages; assert DB state matches requested order with 1 write statement.
Deferred — Unbounded SELECT on pages (missing pagination)
File:
apps/server/src/routes/pages.ts:65-88Severità: MEDIUM (performance)
Soluzione proposta: Add pagination (cursor-based or offset) or a hard cap (e.g., 2000 pages max) with a warning header.
Decision points: Cursor vs. offset; how the frontend sidebar handles pagination (requires frontend changes too).
Trade-off: Breaking change for clients; sidebar tree-building logic must also change.
Test richiesto: Insert 5000 pages; assert response time stays under 200ms and payload under 1MB.
Deferred — Dead
linkstable (schema + indexes with no route)File:
apps/server/src/db/schema.ts:89-100,apps/server/src/db/index.ts:82-87Severità: LOW (dead code)
Soluzione proposta: Either implement the links feature or drop the table via a new migration.
Patch indicativa:
Decision points: Is this feature planned? If yes, leave the schema. If no, migrate it out.
Trade-off: DROP TABLE migration is irreversible. Confirm
linkstable is empty in production before applying.Test richiesto: Run migration on staging; verify no foreign-key errors.
Deferred —
@emoji-mart/datastatic import (1.4 MB on every page load)File:
apps/web/src/components/page/PageChrome.tsxSeverità: MEDIUM (performance)
Soluzione proposta: Lazy-load
@emoji-mart/dataand@emoji-mart/reactinside aReact.lazy/ dynamicimport()triggered only when the icon picker opens.Patch indicativa:
Decision points: Show a loading spinner or skeleton while the picker chunk downloads (~400ms on slow 3G).
Trade-off: Adds a Suspense boundary; first open has a visible delay.
Test richiesto: Lighthouse bundle analysis before/after; initial JS parse time should drop by ~300ms.
Deferred — Test coverage gaps (multiple paths)
Severità: MEDIUM
Decision points: Whether to use Bun test (server) or Vitest+RTL (frontend) for each gap.
Key uncovered paths (highest risk first):
DELETE /api/pages/:id/cover— success, 404, 423 lockedPATCH /api/pages/:idself-parent guard → 400PATCH /api/pages/:idcross-user parentPageId → 404PUT /api/pages/reorderwith locked sibling → 423/approutebeforeLoadauth guard — unauthenticated redirectPOST /api/pages/:id/cover-uploadlocked page → 423updatePropertySchema— empty name, unknown typesignupSchemaname/password upper-bound (100, 1024 chars)Sub-analyst reports
@v6caused workflow failure; (2) logout missing auth middleware (CSRF risk); (3) file upload validates only client-supplied MIME+extension — no magic-byte checkactions/checkout@v6broken tag; (2) N+1 inarchiveRecursive; (3) duplicategetNextSortOrder/getLockedErroracross pages.ts and databases.tsDELETE /coverendpoint zero tests; (2) self-parent guard path untested; (3) cross-user parentPageId reparent untestedGenerated automatically by Codebase Analyst — 2026-05-23