Security
Alpha audit for Elastic Projects. Date: 2026-08-23. Branch: main.
This is not a penetration test. It is a code review against the current alpha scope.
Scope
- Next.js app and API routes
- SQLite replica (
elasticprojects.db) - Local preview and demo mode
- OAuth and web sessions
- Share links and guest join
- Live Spaces sync (where implemented)
Out of scope: Bluesky PDS security, PLC, third-party OAuth providers, your Docker host hardening.
Summary
No critical code flaw found in path handling or SQL binding.
The largest risks are alpha design, not a single bug.
- Spaces are not confidential.
- Public demo mode grants write access to anyone who can hit the host.
- Production on Vercel without Postgres loses all state.
Do not store secrets. Do not expose demo mode on the public internet without understanding that.
High
H1 — No confidentiality on Spaces data
Anyone with a space credential can read permissioned repos. The protocol does not encrypt project files for outsiders.
Mitigation: Treat every project as internal-only. No secrets in files. See limits.md.
H2 — Open local preview on a public host
POST /api/demo/enter sets elastic-demo=1. No password. Any visitor gets demo user you.elasticprojects.test with write access to seeded orgs and projects.
Mitigation: Production hides and rejects demo enter. Local next dev
still offers Open local preview. Override with ALLOW_DEMO=1 only for a
deliberate shared sandbox.
H3 — Share links are secrets
Join tokens are 16-byte random (lib/auth/guest.ts). Guessing is impractical. Leaking the URL is enough to join as a guest and read files after join.
Mitigation: Treat /join/{token} like a password. Rotate by changing the token in app (not automated yet).
H4 — Guest writers on member list
Guests who join via a link can edit and push files. They cannot invite or remove members (canInvite is false for guests).
Mitigation: Only share links with people you trust to write.
Medium
M1 — No CSRF tokens on API POST
State-changing routes use cookies (elastic-session, elastic-demo). Cookies are httpOnly, sameSite: lax, secure when UI_PUBLIC_URL is https.
Cross-site POST from another origin should not send cookies under lax rules. No explicit CSRF token exists.
Mitigation: Keep sameSite: lax. Do not widen to none without secure and a CSRF strategy.
M2 — No rate limits
Demo enter, join, OAuth login, file save, and sync have no throttling. A public host could be scraped or spammed.
Mitigation: Add rate limits before wide exposure. Use Vercel firewall or edge limits when available.
M3 — Session and OAuth in SQLite
Web session tokens are 32 random bytes. Stored as SHA-256 hash (lib/auth/web-session.ts). OAuth state and sessions also live in SQLite.
Mitigation: Protect DATABASE_PATH on disk. Use Postgres with access controls in production. Rotate DATABASE_PATH if the file leaks.
M4 — Live create uses person DID, not org
Live projects are minted under the signed-in account. Not the organization ODS. Ownership model in the product docs does not match live behavior yet.
Mitigation: Use local preview for org-owned dogfood. Do not assume live spaces are org-authoritative.
M5 — Actor search is public
GET /api/actors/search needs no session. It proxies handle search to BSKY_URL.
Mitigation: Low risk. Could add auth or rate limits if abused.
Low
L1 — Demo cookie is a flag
elastic-demo=1 is not bound to a server-side nonce. Setting the cookie (if an attacker could) selects demo mode.
Mitigation: httpOnly prevents client JS from setting it. SameSite reduces cross-site set attempts.
L2 — No Content-Security-Policy header
Next.js default. No custom CSP in next.config.ts.
Mitigation: Add CSP when you add third-party scripts beyond fonts and WebGPU.
L3 — File content is text in React, not HTML
The editor uses textareas (inline-diff-editor.tsx). User markdown is not rendered to HTML in the app. XSS via file body is unlikely today.
Mitigation: Keep file bodies as text. Sanitize if you add markdown preview.
L4 — Path rules on file paths
normalizePath rejects .., .git, and odd characters (lib/repo/paths.ts). API routes decode authority/skey but file paths go through normalizePath.
Mitigation: Keep all file writes behind normalizePath.
What is already sound
| Area | Detail |
|---|---|
| SQL | Kysely parameterized queries. No raw string concat in routes reviewed. |
| Session tokens | 32-byte random, hashed at rest, expiry enforced |
| Join tokens | 128-bit random, format validated |
| Guest DIDs | 152-bit random suffix |
| OAuth callback | Old web session deleted on new login |
| Member removal | Guests cannot remove. Org authority cannot be removed |
| Space credential | DPoP on credential fetch. redirect: error on fetch |
| OAuth HTTP | allowHttp only when UI_PUBLIC_URL is loopback |
Before production deploy
- Wire Neon (or durable Postgres). SQLite on Vercel is not safe or persistent.
- Set
UI_PUBLIC_URLto the real HTTPS origin. - Demo enter is off in production. Use local
npm run devfor preview. - Add rate limits on auth and join routes.
- Do not upload secrets to any project tree.
- Review share links after every external collaborator.
Deploy status
This repository is not deployed from the agent environment. Vercel CLI is not authenticated here.
Deploy from your machine:
npx vercel link
npx vercel env add UI_PUBLIC_URL production
npx vercel --prod
Until Postgres is wired, treat production as a UI preview. Sessions and checkouts will not survive cold starts.
See setup.md for full deploy steps.
Report changes
Update this file when auth, storage, or join flows change.