Skip to main content

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.

Read this first

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):

RepositoryWhat it is
greyscreen-expo-webviewA production Firebase Admin SDK key, private key included
greyscreen-expo-webviewcredentials.json with an iOS certificate path, a plaintext certificate password and a provisioning profile path
gstv-vpsA GCS service-account key; .env, .env.dev, .env.test, .env.prod
gstv-multimediaThe 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 / patternWhat it trusts
POST /signupAny email; an existing account is returned with no credential
POST /loginA user id supplied by the caller
All /users/* mutationsverifyUser, which checks existence only
All Stripe management endpointsverifyUser
GET /users/:idNothing; returns the full profile including FCM tokens and subscriptions
PATCH /users/verifySubcriptionMints a 60-second JWT for any supplied ID
/users/verifySubscriptionTokenSignature 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 / subscriptionData without server verification.
  • The phone's joinSession does not check expiresAt.
  • 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=true bypasses 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/.env and .env.dev point at the production RTDB.
  • Website Firebase identity is branch-selected; merging test into main regresses 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​

SurfaceExposure
RTMP publish and playUnauthenticated
Cache statistics APIUnauthenticated
Streaming CORS*
Media deliveryNo DRM; a URL is enough
Registry passthroughExposed
Admin SSENo 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:

  1. Record variable names and file paths, never values.
  2. Never reproduce a key file, a certificate password or a decoded token.
  3. Never tell someone to copy a production env file to a laptop; ask the maintainer for the non-production one.
  4. .env.template is the only env file that belongs in a commit.
  5. A screenshot of a configuration screen is a secret leak. Crop it.

8. If a key is exposed​

  1. Treat it as compromised now, not after investigation.
  2. Rotate at the provider: GCP service account, Firebase Admin, Stripe, Flutterwave, Twilio, Apple certificate.
  3. Invalidate sessions that depended on it — admin JWTs are signed with SECRET_KEY.
  4. Check audit logs: Cloud audit for VM and storage actions, auditLogs for admin actions, payments and paymentRefs for money.
  5. Purge the value from git history; a follow-up commit that deletes it is not enough.
  6. 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.