Creator Notify v1.0.0

Security

Creator Notify is a small service with a large blast radius if misused: whoever holds a site's API key can send notifications to that site's subscribers. This page states what the code does, what it does not do, and what stays your responsibility.

No absolute security

This document describes a threat model and a set of controls. It is not a guarantee. Review the source, test your deployment, and treat the secret key with the same care as a database password.

What is worth protecting

AssetImpact if exposedStorage
Site secret API keyAnyone can push notifications to that site's subscribersHMAC-SHA-256 with SECRET_PEPPER; plaintext exists only in the response that created it
Subscription auth secretCan decrypt messages addressed to that subscriptionAES-256-GCM envelope, key in SUBSCRIPTION_ENCRYPTION_KEY
Subscription endpoint URLIdentifies a device to the push service; enables targeted pushesStored, never returned by any API response, never logged
VAPID private keyImpersonate your deployment to push servicesWorker secret only
SECRET_PEPPERTurns a database dump into an offline key-cracking exerciseWorker secret only

Threat model

ThreatMitigation in this codebase
Cross-tenant access (site A reads or notifies site B)Tenant identity is derived only from the hashed API key. Every query filters by site_id. No endpoint accepts a caller-supplied site id for writes.
Stolen API keyRotation (rotate-key), revocation (revoke-key), key prefixes/hints for identification, per-site rate limits to bound abuse.
Database dumpKeys are peppered hashes; subscription auth secrets are encrypted; endpoints are stored but never exposed through the API.
InjectionAll SQL uses bound parameters. JSON bodies reject __proto__, prototype and constructor keys, and are depth/size limited.
XSS via notification payloadServer rejects control characters and non-HTTP(S) URLs; the Service Worker validates every URL against an origin allow-list before navigation.
Open redirect via urlNavigation is limited to the site's own origin (plus origins explicitly configured in the worker).
Notification spam from a third-party pageSubscription creation requires the registered origin; per-IP and per-site rate limits; endpoint host allow-list; payload size limits.
Resource exhaustionBounded concurrency, bounded batch sizes, per-site subscription caps, request body ceilings, retention windows.
Credential leakage in logsError logging emits an error name/message and request id only; endpoint URLs, keys and SQL are never logged. Stack traces are suppressed when ENVIRONMENT=production.
Cross-site request from a browserManagement endpoints send no CORS headers at all; browser-facing endpoints only reflect registered origins. Auth is still required — CORS is a browser rule, not an auth check.

Credential handling

API keys

stored:   HMAC-SHA-256(key = SECRET_PEPPER, message = api_key)
returned: once, in the 201 response of /v1/sites/register or /v1/sites/rotate-key
lookup:   hash the presented key, then SELECT ... WHERE secret_key_hash = ?
  • Keys are 40 base62 characters behind a cn_live_ prefix (≈238 bits of entropy) — unguessable and easy to spot in secret scanners.
  • Comparison happens inside SQL against an indexed hash, so there is no secret-dependent branching in application code.
  • api_key_prefix and api_key_hint are non-secret identifiers for operator support; they cannot be used to authenticate.

Subscription auth secrets

envelope: v1.<base64url(iv)>.<base64url(ciphertext+tag)>
alg:      AES-256-GCM, 96-bit random IV per record, 128-bit tag
AAD:      the subscription endpoint URL

Binding the ciphertext to the endpoint prevents an attacker with database write access from copying an encrypted secret between rows: decryption fails when the endpoint does not match.

What is not encrypted

p256dh (the subscriber's public key) and the endpoint URL are stored in plaintext because they are needed to build each push request. The endpoint is treated as sensitive: it is never returned by the API and never written to logs.

Origins and CORS

  • The Origin header is used to decide which registration a browser may address, and it must exactly equal the registered origin.
  • An Origin or Referer header is never treated as proof of authorization for a write.
  • Management endpoints emit no Access-Control-Allow-Origin; a browser cannot use a key it does not have, and a compromised page cannot read responses.
  • CORS_ALLOWED_ORIGINS (comma separated) extends the public allow-list — for example a documentation demo origin. It does not grant any authorization.

Rate limiting and abuse prevention

Fixed windows stored in the service database, one row per scope/subject/window, incremented with a single atomic upsert. Because the counter lives in the database, limits apply across Worker isolates and colo regions rather than per process.

  • Anonymous endpoints are keyed by client IP; authenticated endpoints by site id.
  • Subscription endpoints are keyed by site + IP, so one abuser cannot exhaust a whole tenant's budget or lock out other users of the same site.
  • Counters are purged by the cron job after RATE_LIMIT_RETENTION_HOURS.
  • Set REGISTRATION_TOKEN to stop strangers registering origins on a public deployment.
  • An operator can layer a CDN or WAF rate limit in front of these — recommended for public deployments.

Your deployment responsibilities

  • Secret storage. Use your platform's encrypted secret store (or your CI secret store). Never commit .dev.vars or .env.
  • Key custody on your servers. Environment variables or a secrets manager, readable only by the app user. Not in web-root files, not in JS bundles.
  • Transport. Serve your site and the API over HTTPS only. HSTS is recommended.
  • Access to the operator account. Restrict console and API-token access; the operator can read tenant data for every site on the instance.
  • Registration policy. Set REGISTRATION_TOKEN unless you intend public registration.
  • Logs. Worker logs contain request ids and error names. Configure retention to match your policy.
  • Retention. Tune the retention variables to your jurisdiction's requirements.
  • Incident response. Rotate the affected site key, then the platform secrets if you suspect the deployment itself was compromised.

If a key leaks

  1. POST /v1/sites/rotate-key with the leaked key (it still works until you rotate) — the old key dies instantly.
  2. If the attacker already rotated the key, you no longer control the registration: use revoke-key from a session that still authenticates, or ask the operator to suspend the registration.
  3. Review GET /v1/notifications for messages you did not send.
  4. If SECRET_PEPPER itself leaked, rotate it and re-issue every site key (existing hashes become unverifiable by design).

Reporting a vulnerability

See SECURITY.md in the repository for the disclosure process. Please do not open a public issue for a live vulnerability.