Threat Model
Playupi’s attack surface includes:
Surface Risk Priority Game iframes Malicious game code, XSS via iframe escape, clickjacking High Event ingestion API Spam, fake metrics, bot traffic High Admin dashboard Unauthorized access, privilege escalation Critical User input XSS via search, game titles, descriptions Medium Infrastructure Data leaks, unpatched dependencies, misconfigurations Medium
Iframe Security
Games run inside iframes from external sources. This is the highest-risk surface.
Sandbox Attributes
All game iframes must use restrictive sandbox attributes:
sandbox = " allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox "
referrerpolicy = " no-referrer "
Attribute Why allow-scriptsGames need JavaScript to run allow-same-originRequired for some game engines to function (localStorage, cookies) allow-popupsSome games open links (e.g., developer website) allow-popups-to-escape-sandboxAllows popup windows to not inherit sandbox Omitted: allow-forms Games should not submit forms to external servers Omitted: allow-top-navigation Prevents iframe from redirecting the parent page
Content Security Policy
frame-src https://*.gamedistribution.com https://*.html5games.com;
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
connect-src 'self' https://api.playupi.com;
Directive Purpose frame-srcWhitelist allowed iframe sources (broker domains) frame-ancestors 'none'Prevent Playupi from being embedded in other sites (clickjacking) connect-srcRestrict API calls to own domain
Iframe Communication
Never use window.postMessage with * as target origin
Validate message origin against a whitelist of broker domains
Treat all messages from iframes as untrusted input
window . addEventListener ( ' message ' , ( event ) => {
if ( ! ALLOWED_ORIGINS . includes (event . origin )) return
const data = validateBrokerMessage (event . data )
// Process validated message
Admin Authentication
Auth Flow
POST /api/admin/auth/login { email, password }
Verify password (bcrypt, cost factor 12)
Issue JWT (short-lived: 1 hour)
Store refresh token (httpOnly, secure, sameSite=strict cookie)
JWT included in Authorization header for all admin requests
Token Security
Token Storage Lifetime Refresh Access token (JWT) Memory (JS variable) 1 hour Via refresh token Refresh token httpOnly secure cookie 7 days Re-login required
Password Requirements
Rule Value Minimum length 12 characters Hashing bcrypt with cost factor 12 Brute force protection Account lockout after 5 failed attempts (15 min cooldown)
Session Invalidation
Logout clears refresh token cookie and invalidates server-side
Password change invalidates all existing sessions
Admin can revoke other admin sessions
Rate Limiting
Public API
Endpoint Limit Window Key GET /api/v1/games60 req 1 min IP GET /api/v1/search30 req 1 min IP POST /api/v1/events100 req 1 min IP POST /api/v1/events500 events 1 min Session ID POST /api/v1/games/:id/like10 req 1 min IP
Admin API
Endpoint Limit Window Key POST /api/admin/auth/login5 req 15 min IP All admin endpoints 120 req 1 min User ID
Implementation
Use Redis-based sliding window rate limiting:
async function rateLimit ( key : string , limit : number , windowMs : number ) {
const current = await redis . incr (key)
await redis . pexpire (key , windowMs)
throw new RateLimitError ()
Return 429 Too Many Requests with Retry-After header.
General Rules
Validate all input at the API boundary
Use schema validation (Zod) for request bodies
Sanitize strings before database insertion
Never trust client-side validation alone
const searchSchema = z . object ( {
q: z . string () . min ( 2 ) . max ( 100 ) . trim () ,
type: z . enum ([ ' games ' , ' categories ' , ' authors ' ]) . optional () ,
limit: z . number () . int () . min ( 1 ) . max ( 20 ) . default ( 5 )
const updateGameSchema = z . object ( {
title: z . string () . min ( 1 ) . max ( 200 ) . trim () . optional () ,
description: z . string () . max ( 5000 ) . trim () . optional () ,
visibility: z . enum ([ ' draft ' , ' hidden ' , ' visible ' ]) . optional () ,
categories: z . array (z . string () . uuid ()) . max ( 10 ) . optional () ,
iframeUrl: z . string () . url () . optional ()
Iframe URL Validation
Before storing or rendering an iframe URL:
function validateIframeUrl ( url : string ) : boolean {
const parsed = new URL (url)
if (parsed . protocol !== ' https: ' ) return false
// Must be from an allowed domain
if ( ! ALLOWED_IFRAME_DOMAINS . some ( d => parsed . hostname . endsWith (d))) return false
// No javascript: or data: protocols in the path
if ( / ^ (javascript | data): / i . test (url)) return false
XSS Prevention
Output Encoding
All dynamic content rendered via React (auto-escapes by default)
Never use dangerouslySetInnerHTML with user-provided content
Game descriptions and instructions stored as plain text , rendered with whitespace preservation — not as HTML or Markdown
X-Content-Type-Options: nosniff
X-XSS-Protection: 0 // Deprecated but harmless
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
CORS Configuration
API Allowed Origins Methods Public (/api/v1) https://playupi.comGET, POST Admin (/api/admin) https://admin.playupi.comGET, POST, PATCH, DELETE Preview (dev only) https://*.vercel.appAll
Credentials are not sent cross-origin (JWT is in Authorization header, not cookies)
Access-Control-Max-Age: 86400 to reduce preflight requests
Preview deploy origins are only allowed in non-production environments
See API Reference → CORS Policy for the full header configuration
CSRF Protection
Admin API uses JWT in Authorization header (not cookies) for state-changing requests — inherently CSRF-resistant
Refresh token cookie uses SameSite=Strict
Public event API is idempotent and uses session IDs — no CSRF risk
Event Anti-Abuse
Bot Detection
Signal Action No JavaScript execution (headless) Block at CDN level (Cloudflare Bot Management) Events from non-existent sessions Drop silently Timestamp drift > 24h from server time Drop event Identical events within 1s window Deduplicate > 500 events/min from single session Rate limit
Metric Integrity
Events are append-only — no client can modify or delete past events
Aggregation jobs process events idempotently (re-running produces the same result)
Anomaly detection flag: if a game’s DAU spikes > 10x overnight without ranking change, flag for review
Fingerprinting
Anonymous device fingerprinting uses a combination of:
Random UUID stored in localStorage (primary)
Fallback: basic browser fingerprint (screen size, timezone, language) hashed
This is used for session continuity and deduplication, not for cross-site tracking.
Data Privacy
MVP (Pre-GDPR)
Rule Implementation No personal data collected No accounts, no emails, no names Anonymous tracking only Device fingerprint (random UUID in localStorage) No third-party trackers No Google Analytics, no Facebook Pixel Cookie-less Only httpOnly cookies for admin auth
Future (GDPR Compliance)
When user accounts are added:
Requirement Implementation Consent banner Before any tracking begins Data export API endpoint to export all data for a user Right to deletion API endpoint to delete all user data Data retention Clear events older than retention policy Privacy policy Accessible from footer on all pages DPA with providers Data Processing Agreements with all third-party services
See Decisions for GDPR status.
Dependency Security
Practice Frequency npm auditOn every CI run Dependabot / Renovate Enabled for automated PRs Lock file committed Always (package-lock.json or pnpm-lock.yaml) Review new dependencies Before adding — check bundle size, maintenance status, license Pin major versions Avoid unexpected breaking changes
Supply Chain
Use npm with --ignore-scripts during CI to prevent install-time attacks
Pin exact versions for critical dependencies (ORM, auth, crypto)
Audit transitive dependencies periodically
Infrastructure Security
Secrets Management
Secret Storage Access Database URL Environment variable Backend only Redis URL Environment variable Backend only JWT signing key Environment variable Backend only Admin initial password One-time setup, not stored in code N/A Broker API keys Environment variable Backend only
Never commit secrets to git. Use .env.local for development, platform-native secret management for production (Vercel env vars, Railway secrets, etc.).
Network
Rule Implementation HTTPS everywhere Enforced at CDN/proxy level Database not publicly accessible Internal network only, accessed via connection string Admin API on separate subdomain admin-api.playupi.com — easier to restrict at network levelSSH/console access Key-based only, no password auth
Logging
What to log What NOT to log Authentication attempts (success + failure) Passwords or password hashes Admin actions (game create, visibility change, delete) Full request bodies with sensitive data Rate limit triggers Session tokens or JWTs Error stack traces (server-side only) User IP addresses (hash them) Unusual event patterns
Incident Response
Severity Levels
Level Example Response Time Action P0 — Critical Data breach, admin auth bypass, full site down Immediate All hands, status page update, public comms if user data affected P1 — High Event ingestion down, ranking broken, partial outage < 1 hour On-call investigates, hotfix deploy P2 — Medium Elevated error rate, degraded performance, bot spam < 4 hours Investigate during business hours P3 — Low Minor UI bug, non-critical feature broken Next sprint Track in issue tracker
Response Steps
Detect — Sentry alerts, Betterstack uptime, user reports
Assess — Determine severity, scope, and blast radius
Contain — Block attack vector (rate limit, disable endpoint, revoke tokens)
Fix — Deploy hotfix or rollback
Review — Post-incident write-up within 48 hours (what happened, why, how to prevent)
Maintain a simple on-call list (even if it’s just 1-2 people) with phone numbers and escalation order. Store it outside the platform itself (e.g., shared note, team chat pinned message).
Security Checklist (Pre-Launch)
Item Status All iframes use sandbox attributes CSP headers configured Admin auth with bcrypt + JWT Rate limiting on all public endpoints Input validation with Zod on all routes Iframe URL whitelist enforced Security headers set (X-Frame-Options, etc.) npm audit passes with 0 criticalNo secrets in git history HTTPS enforced Database not publicly accessible Admin actions logged Error pages don’t leak stack traces