Security notes
This page describes the platform as it is. It does not contain a single secret value, and it should stay that way: names and paths, never contents.
Several issues below are known and unfixed. Do not build a new feature on top of a pattern listed here without a decision recorded somewhere, and do not assume a client-side check is enforced anywhere.
1. Credentials in git — critical
Confirmed tracked files (values not reproduced):
| Repository | What it is |
|---|---|
greyscreen-expo-webview | A production Firebase Admin SDK key, private key included |
greyscreen-expo-webview | credentials.json with an iOS certificate path, a plaintext certificate password and a provisioning profile path |
gstv-vps | A GCS service-account key; .env, .env.dev, .env.test, .env.prod |
gstv-multimedia | The same GCS key; .env |
Also on disk but gitignored: Firebase Admin JSONs in both API repositories, .env.*.local with
Twilio/Flutterwave/Stripe values, Android/iOS keystores, and the high-privilege orchestrator key
in gstv-secrets.
Treat every one of these as compromised regardless of rotation status. The remediation is
rotation, history purge, Secret Manager with actual reads at boot, no baked Firebase key in the
worker image, and removal of credentials.json from the phone repository.
For documentation and support: never paste a key file, an .env file or a decoded token
into an issue, a chat or a page. Variable names and file paths only.
2. Customer API identity is not authentication — critical
| Endpoint / pattern | What it trusts |
|---|---|
POST /signup | Any email; an existing account is returned with no credential |
POST /login | A user id supplied by the caller |
All /users/* mutations | verifyUser, which checks existence only |
| All Stripe management endpoints | verifyUser |
GET /users/:id | Nothing; returns the full profile including FCM tokens and subscriptions |
PATCH /users/verifySubcription | Mints a 60-second JWT for any supplied ID |
/users/verifySubscriptionToken | Signature only |
There is no rate limiting anywhere on the customer API.
Impact: knowing an email or a user ID is effectively equivalent to being that user. Subscription entitlement is forgeable.
Direction: require a verified Firebase ID token on /users/*, /me and /pay/stripe/*;
bind the acting user to the token; remove or hard-gate legacy signup/login and
verifySubcription; add rate limiting.
The /me* routes are the model to copy: requireFirebaseAuth / optionalFirebaseAuth verify
with verifyIdToken.
3. TV pairing RTDB is world-writable — critical
tv-pairing/sessions/$sessionId has .read: true, .write: true.
- The TV applies
userData/subscriptionDatawithout server verification. - The phone's
joinSessiondoes not checkexpiresAt. - The QR is unsigned and
pin === sessionId. - There is no server authority, collision control, rate limit or audit.
Impact: session enumeration, command injection, TV/phone impersonation, account and subscription forgery.
Direction: lock rules to authenticated users, mediate session creation and validation server-side, enforce expiry and rate limits, verify user and subscription data before the TV applies it, and implement the missing command handlers or remove the remote feature.
4. Admin takeover paths — critical to high
- Default admins are seeded on first boot, gated only by
meta/bootstrap. ADMIN_AUTH_DISABLED=truebypasses authentication and is not blocked in production.setupAdmins()swallows errors, so an API can run with no usable admin.- The admin API's Flutterwave webhook has no dedupe and does not await several writes, so a replayed delivery can duplicate payments and subscriptions.
- Two subscription handlers never respond on error.
Direction: force rotation, seed only in non-production, reject ADMIN_AUTH_DISABLED in
production, add webhookEvents dedupe, fix the hanging handlers.
5. Environment and payment inconsistencies — high
- The test stack shares the production Firebase project. Isolation is a Compose profile plus
RESOLUTION_CONSUMER_DISABLED, not a separate database. gstv-vps/.envand.env.devpoint at the production RTDB.- Website Firebase identity is branch-selected; merging
testintomainregresses production. - Flutterwave uses the production merchant in every environment.
- Plan priority conventions disagree between code and comments.
- Test env files contain leftover placeholders.
The operational consequence is simple: a write through the test stack is a production write.
6. Open surfaces
| Surface | Exposure |
|---|---|
| RTMP publish and play | Unauthenticated |
| Cache statistics API | Unauthenticated |
| Streaming CORS | * |
| Media delivery | No DRM; a URL is enough |
| Registry passthrough | Exposed |
| Admin SSE | No per-user connection cap and no overall timeout |
7. Secrets policy for this documentation
The rules this site follows, and that any contributor should follow:
- Record variable names and file paths, never values.
- Never reproduce a key file, a certificate password or a decoded token.
- Never tell someone to copy a production env file to a laptop; ask the maintainer for the non-production one.
.env.templateis the only env file that belongs in a commit.- A screenshot of a configuration screen is a secret leak. Crop it.
8. If a key is exposed
- Treat it as compromised now, not after investigation.
- Rotate at the provider: GCP service account, Firebase Admin, Stripe, Flutterwave, Twilio, Apple certificate.
- Invalidate sessions that depended on it — admin JWTs are signed with
SECRET_KEY. - Check audit logs: Cloud audit for VM and storage actions,
auditLogsfor admin actions,paymentsandpaymentRefsfor money. - Purge the value from git history; a follow-up commit that deletes it is not enough.
- Record what happened in the incident log, with names and times, not values.
9. What is genuinely solid
To keep the picture honest: request validation at boot is strict and fails closed; Stripe
webhooks use raw-body signatures plus create-only dedupe; the readiness transition is
transactional and idempotent; notification writes are guarded; deletion phases are idempotent;
catalogue responses deliberately avoid long shared caching so a deleted title cannot linger;
/me* verifies Firebase tokens. The architecture has good bones. The open items are identity,
pairing, and secrets — not the data path.
Next: back to Architecture or Running locally.