fix: only send admin API key for authenticated sessions #39
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-docs-app!39
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "38-fix-ssr-admin-token-leak"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
apiFetch()sentX-PaleDocs-Tokenon every server-side request, exposing private notes/projects/boards to anonymous visitors/signin?admin=trueChanges
src/lib/api.ts: AddedApiFetchOptionsinterface withauthenticatedflag; updatedapiFetch()and all 18 exported API functions to only include the admin token when explicitly opted insrc/lib/slugCache.ts: ThreadedApiFetchOptionsthrough tolistNotes()callsrc/routes/+layout.server.ts,+page.server.ts: Check session and pass{ authenticated }to API callssrc/routes/notes/+page.server.ts,notes/[slug]/+page.server.ts,notes/[slug]/edit/+page.server.ts: Session-gated auth optionssrc/routes/boards/+page.server.ts,boards/[slug]/+page.server.ts: Session-gated auth optionssrc/routes/dashboard/+page.server.ts: Session-gated auth options for all board/item fetchessrc/routes/projects/+page.server.ts,projects/[slug]/+page.server.ts: Session-gated auth optionssrc/routes/search/+page.server.ts,tags/+page.server.ts,tags/[name]/+page.server.ts,repos/+page.server.ts: Session-gated auth optionssrc/routes/api/notes/+server.ts,api/notes/[slug]/+server.ts: Write proxies always pass{ authenticated: true }src/routes/api/boards/[slug]/items/+server.ts,api/boards/[slug]/items/[id]/+server.ts: Write proxies always pass{ authenticated: true }src/routes/signin/+page.server.ts: AddedadminModeflag from?admin=truequery paramsrc/routes/signin/+page.svelte: Replaced Keycloak redirect with "Contact Lucas" page linking to portfolio; admin login via query parame2e/public-readiness.spec.ts: 11 new E2E tests verifying anonymous visitors see no private content and sign-in page shows contact messagingTest Plan
npx svelte-check-- 0 errors, 2 pre-existing warningsnpx eslint .-- cleannpm run build-- succeedsnpx playwright test e2e/auth.spec.ts-- all 13 existing tests pass against live sitenpx playwright test e2e/public-readiness.spec.ts-- tests correctly detect the bug on current production (proving private content is exposed); will pass once deployedReview Checklist
Related
phase-pal-e-docs-f14-public-readinessSelf-Review
Reviewed all 23 changed files (429 additions, 124 deletions).
Verdict: PASS -- no blocking issues
Findings
Core fix is correct.
apiFetch()defaults to not sendingX-PaleDocs-Token. Every page server load function checks for a Keycloak session and only passesauthenticated: truewhen one exists. All write-operation API proxies always passauthenticated: truesince they are already session-gated.Note on
slugCache.ts: The slug cache is a server-side singleton shared across requests. If an authenticated user populates the cache, an anonymous user hitting the same server within 60s could see private note slugs in the cached list. This is not a data leak -- slugs are only used for auto-linking in rendered content, and the note-detail endpoint itself filters by auth. The comment in the file documents this tradeoff.Sign-in page: Clean two-mode implementation. Default shows "Contact Lucas" with portfolio link.
?admin=trueshows Keycloak form. Back link from admin mode to default.E2E tests: 11 new tests cover all acceptance criteria. Tests correctly fail against current production (proving the bug exists) and will pass once deployed.
Checklist
svelte-check0 errorseslintcleannpm run buildsucceedsPR #39 Review
DOMAIN REVIEW
Tech stack: SvelteKit / TypeScript / Playwright E2E. This is a security fix -- SSR was sending the admin API key (
X-PaleDocs-Token) on every server-side request regardless of session state, exposing private notes/projects/boards to anonymous visitors.Architecture approach: Clean and correct. The fix introduces an
ApiFetchOptionsinterface with anauthenticatedboolean, threaded through all 18 exported API functions and into every+page.server.tsloader. The token is only attached whenoptions?.authenticatedis true. This is a secure-by-default design -- callers must explicitly opt in to sending the admin key.SvelteKit patterns:
authenticatedfrom!!session?.uservialocals.auth()./api/notes,/api/boards/...) correctly hardcode{ authenticated: true }since they already guard with a401check at the top.notes/[slug]/edit/+page.server.ts) correctly uses{ authenticated: true }since it is already behind aredirect(302, '/signin')guard.+layout.server.tsroot loader also correctly gateslistProjects()-- this means the sidebar/nav project list is also filtered for anonymous users.Sign-in page redesign: Well done. The default
/signinshows a "Contact Lucas" page with a link to the portfolio. Admin login is tucked behind/signin?admin=true. TheadminModeflag is derived server-side from a query param, which is safe -- it only controls which UI variant to show; the actual Keycloak auth flow still requires valid credentials.Test coverage: 11 new E2E tests in
e2e/public-readiness.spec.tscovering:These complement the existing 13 auth boundary tests in
e2e/auth.spec.tswhich cover write-op protection (401s, UI element visibility).BLOCKERS
None.
DRY concern in
deleteBoardItem-- evaluated but not a blocker. ThedeleteBoardItemfunction duplicates the auth header logic fromapiFetchinstead of callingapiFetch. This duplication exists becausedeleteBoardItemdoes not parse a JSON response body (it is avoidreturn). The duplicated code is 4 lines (checkoptions?.authenticated, read env, set header) which matchesapiFetchexactly. This is a pre-existing pattern that this PR simply extended with theoptionsparameter. It is not divergence-prone because both paths are in the same file, 140 lines apart, and the logic is trivially identical. Flagging as a nit below rather than a blocker.Slug cache concern -- evaluated but not a blocker. The
slugCache.tscomment acknowledges that an authenticated request can populate the cache with private slugs, which a subsequent anonymous request would then see from cache. However, the comment correctly notes that the note-detail endpoint itself filters, so an anonymous user seeing a private slug in an autolink would get a 404/empty response when clicking it. This is a defense-in-depth gap, not a data leak. The 60-second TTL limits the window. Flagging as a nit.NITS
deleteBoardItembypassesapiFetch(/home/ldraney/pal-e-app/src/lib/api.tslines 300-322): This function duplicates the auth header logic instead of routing throughapiFetch. Consider refactoringapiFetchto handlevoidresponses (e.g., checkContent-Lengthor204status before callingres.json()), then havedeleteBoardItemuseapiFetch<void>. This would eliminate the only DRY violation in the auth path.Slug cache can leak private slugs to anonymous users (
/home/ldraney/pal-e-app/src/lib/slugCache.tslines 21-49): If an authenticated request populates the cache, a subsequent anonymous request within the 60-second TTL window will receive private slugs for autolink resolution. The mitigation (note-detail endpoint filters) is sound, but a cleaner approach would be to key the cache by auth state (e.g., separatepublicSlugs/allSlugscaches). Low priority since there is no actual data exposure.CSS duplication between
.contact-btnand.login-submit(/home/ldraney/pal-e-app/src/routes/signin/+page.sveltelines 51-82): These two classes share 7 identical property declarations. Consider extracting a shared.btn-primaryclass or using a common base. Non-blocking since they are scoped to this one component.Portfolio URL is hardcoded in the Svelte template (
/home/ldraney/pal-e-app/src/routes/signin/+page.svelteline 31):https://portfolio.tail5b443a.ts.netfollows the same Tailscale domain pattern used elsewhere in the codebase (auth.ts), so this is consistent. However, if the Tailscale node name ever changes, it would need updating in multiple files. Consider centralizing Tailscale domain URLs. Very low priority.SOP COMPLIANCE
38-fix-ssr-admin-token-leakreferences issue #38phase-pal-e-docs-f14-public-readinessandCloses #38env.PAL_E_DOCS_API_KEY(runtime env var), no secrets in codefix: only send admin API key for authenticated sessionsPROCESS OBSERVATIONS
authenticatedoption. The secure-by-default design (token omitted unless explicitly opted in) means any missed call site fails safe (shows public content only, never leaks private content).public-readiness.spec.ts) is designed to validate the fix post-deploy -- the PR body correctly notes that tests "correctly detect the bug on current production" and will pass once deployed.VERDICT: APPROVED