EP

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

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.

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

  1. Wire Neon (or durable Postgres). SQLite on Vercel is not safe or persistent.
  2. Set UI_PUBLIC_URL to the real HTTPS origin.
  3. Demo enter is off in production. Use local npm run dev for preview.
  4. Add rate limits on auth and join routes.
  5. Do not upload secrets to any project tree.
  6. 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.