Security fixes are released on main and deployed to the current production
version of UIUXskills. Older deployments, forks, and unmaintained commits are
not supported.
| Version | Security support |
|---|---|
Current production deployment from main |
Supported |
| Earlier deployments and commits | Not supported |
The production runtime requires Node.js 22 or later. Keep Next.js, Firebase, and the production dependency graph current through the release process below.
Report suspected vulnerabilities privately to
uiuxofai@gmail.com with the subject
[SECURITY] UIUXskills. Include affected URLs or code paths, a safe
reproduction, impact, and any suggested mitigation. Do not open a public issue
or include secrets, user data, or exploit payloads in a public channel.
The maintainer will acknowledge a report within two business days, provide a status update at least weekly while it is being investigated, and coordinate a fix and disclosure timeline with the reporter. Reports that are out of scope or not reproducible will receive a reasoned response. If a credential may be exposed, say so in the first line so its rotation can start immediately.
- JSON-LD is serialized through
serializeJsonForHtml, which escapes markup delimiters before data reaches an HTML script element. The regression suite also checks every JSON-LDdangerouslySetInnerHTMLsink. - The CSP in
next.config.tsis currently report-only. It omitsunsafe-evaland retains the required restrictive directives, but it must not be changed to enforced mode until the browser smoke matrix indocs/security/RELEASE-CHECKLIST.mdpasses, including Firebase redirect sign-in and generation polling. - Provider-controlled public-page and screenshot retrieval uses a shared SSRF-safe fetch boundary: only public HTTP(S) destinations are accepted, every redirect hop is revalidated, connections are pinned to validated DNS answers, and response bodies, redirects, and time are bounded. Binary image responses also require an allowlisted content type with a matching file signature. Fixed first-party/provider API endpoints are separate trusted service boundaries.
- Logs, persisted operational failures, and error responses retain only stable application codes, allowlisted exception types, bounded non-secret fields, and credential-free URL origins. Provider exception text, prompts, request bodies, tokens, stacks, and log-control characters must not cross this diagnostic boundary; legacy job errors are redacted again when projected.
- Magic-link requests are limited to 5 per 10 minutes by IP and 3 per hour by
normalized email. Email identifiers are HMAC-hashed before entering Redis.
Anonymous clients receive one free generation per IP, with a separate hard
abuse ceiling of 3 attempts per hour; signed-in users receive 10 generations
per hour and editors are exempt. Production limiter configuration and Redis
failures fail closed with a controlled
503 rate_limit_unavailableresponse. - Generation uploads have declared-body, file-size, image-signature, decode, and pixel-count limits. Job polling requires the owning session or the matching anonymous cookie; unauthorized and unknown job IDs both return 404. Public generation errors use stable codes rather than provider or database diagnostics.
- Account identities are never merged automatically across Firebase UIDs, even when email addresses match. Post-auth destinations are restricted to same-origin internal paths.
| Service | Security boundary | Secret handling |
|---|---|---|
| Firebase Auth | Browser sign-in uses Firebase; server sessions verify Firebase ID tokens with the Admin SDK. Account-link conflicts require an explicit linking flow. | FIREBASE_ADMIN_CREDENTIALS_B64 is server-only. Never expose it or any service-account JSON to the browser. |
| Supabase Postgres and Storage | Drizzle connects as the table owner; RLS is enabled deny-by-default on application tables. The transaction pooler requires prepared statements to stay disabled. | Keep DATABASE_URL and SUPABASE_SERVICE_ROLE_KEY server-only. The service-role key bypasses RLS and is only for server storage operations. |
| Upstash QStash | Production worker deliveries are signature-verified; the internal-token and inline-dispatch fallbacks are limited to development/test. | QSTASH_TOKEN, QSTASH_CURRENT_SIGNING_KEY, and QSTASH_NEXT_SIGNING_KEY are required in production. |
| Upstash Redis | Redis enforces generation and magic-link abuse limits. | UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, and RATE_LIMIT_SECRET are required in production; do not allow a production fail-open fallback. |
| Production cron routes | /api/cron/warm-db and /api/cron/supervise-batches require Authorization: Bearer <CRON_SECRET>. Missing or invalid credentials return 401; a production configuration failure returns a non-sensitive error. |
Configure the identical CRON_SECRET in Vercel and GitHub Actions. GitHub workflows stop before making a request when it is absent. |
src/lib/env.ts enforces DATABASE_URL in every environment and the cron and
rate-limit controls below in production. The remaining values are required when
their server-side features are enabled. Set secrets in Vercel (and the matching
GitHub Actions secret where noted), never in source or NEXT_PUBLIC_* variables.
| Variable | Requirement |
|---|---|
DATABASE_URL |
Required in every environment; use the Supabase transaction-pooler URL in production. |
CRON_SECRET |
Required in production; use the same value for the Vercel app and GitHub Actions workflows. |
UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN |
Required in production for rate limiting. |
RATE_LIMIT_SECRET |
Required in production; HMAC key for non-reversible email limit keys; at least 32 characters. |
FIREBASE_ADMIN_CREDENTIALS_B64 |
Required for production server-side Firebase session verification and magic-link administration. |
QSTASH_TOKEN |
Required in production for durable worker dispatch. |
QSTASH_CURRENT_SIGNING_KEY / QSTASH_NEXT_SIGNING_KEY |
Required in production and whenever QSTASH_TOKEN is configured. |
INLINE_TASKS |
Must be false in production. It is an explicit development/test-only local dispatch mode. |
Firebase NEXT_PUBLIC_* configuration |
Required for browser Firebase sign-in; these values are public identifiers, not secrets. |
Provider API keys and optional storage credentials must also be set for each enabled production feature. Missing production rate-limit credentials must take the affected public endpoint out of service; they must never make it unlimited.
- Contain the incident: revoke or rotate the exposed credential at its issuer (Firebase, Supabase, Upstash, Vercel, or the relevant AI/email provider).
- Replace the value in Vercel; for
CRON_SECRET, replace the identical GitHub Actions secret in the same maintenance window. For QStash, rotate the token and both signing keys together. - Redeploy production, then verify affected authentication, queue, cron, and rate-limit behavior without logging the replacement secret.
- Review Vercel, provider, and application logs for use of the exposed value; invalidate affected sessions or tokens when the provider supports it.
- Record the incident, scope, rotation time, and follow-up in the private incident record. Do not commit credentials or paste them into issues.
pnpm audit --prod currently has no critical or high advisories. Two moderate
uuid advisories (GHSA-w5hq-g745-h8pq) remain through
firebase-admin@14.2.0 > @google-cloud/storage@7.19.0:
uuid@8.3.2directly under Cloud Storage.uuid@9.0.1throughgaxios@6.7.1.
The patched UUID floor is outside those parents' declared compatibility ranges.
This application imports only firebase-admin/app and firebase-admin/auth,
not Firebase Admin Storage, and exploitation additionally requires a Storage
path to call UUID v3/v5/v6 with an attacker-controlled output buffer. This is
an accepted release residual, not an audit suppression or permanent exception.
Recheck the paths on every Firebase Admin/Cloud Storage release and remove the
entry when upstream supports a patched UUID version.
The SSRF boundary rejects the IANA special-use IPv4 and IPv6 ranges encoded in
src/lib/security/safe-fetch.ts, including private, loopback, link-local,
documentation, benchmarking, multicast, and reserved space. That registry is
static rather than fetched at runtime, by design: a network dependency must
not decide whether another network request is safe. Registry drift is an
accepted release residual. Recompare the encoded ranges with the authoritative
IANA registries during security releases and whenever address policy changes;
add newly reserved ranges before relying on the boundary for them.
Before every production release, run the commands and service checks in
docs/security/RELEASE-CHECKLIST.md.
Do not claim CSP enforcement, a production build, an authenticated browser
flow, or an authenticated cron smoke passed without fresh evidence from that
checklist.