🔒 QuickVote Threat Model

Security Analysis & Architecture Documentation

Executive Summary

QuickVote is a real-time web-based polling application designed for live voting in meetings, classrooms, and presentations. This threat model identifies security risks across the application's architecture, including the React SPA frontend, Supabase backend-as-a-service, real-time WebSocket communications, image slide uploads via Supabase Storage, a separate presentation window for projected content, session templates, and team-based voting with per-team result filtering.

Application Type

Single Page Application (SPA) with real-time features

Backend

Supabase (PostgreSQL + Realtime)

Authentication

Anonymous JWT-based

Hosting

Vercel (Static CDN)

Architecture Diagram

The following diagram illustrates the main components, data flows, and trust boundaries in the QuickVote system.

UNTRUSTED ZONE (Internet) TRUSTED ZONE (Supabase BaaS) Admin (Session Creator) Participant (Anonymous Voter) Browser (React SPA) Admin Dashboard Participant View Zustand Store Realtime Hooks Optional Password Gate Vercel CDN Static SPA Hosting HTTPS Enforced Supabase Auth Anonymous Authentication JWT Token Generation auth.uid() → User Identity Supabase Realtime Broadcast Events Presence Tracking Postgres Changes (CDC) PostgreSQL Database sessions id, admin_token, status questions text, type, options, status votes value, reason, participant batches name, position, status Row Level Security (RLS) Policies RLS Policy Enforcement • Sessions: creator can UPDATE/DELETE • Questions: session creator only • Votes: participant_id = auth.uid() • Batches: session creator only • All tables: authenticated SELECT HTTPS Request Static Assets 1. signInAnonymously() 2. JWT Token WebSocket Subscribe Events / Presence DB Queries (with JWT) Query Results CDC ⚠️ Attack Surface T1: Admin token in URL (guessable) T2: Client-side password gate (bypassable) T3: Session ID enumeration T4: XSS via user-generated content T5: Vote manipulation (client controls) T6: DoS via large import files T7: Storage bucket abuse (image uploads) T8: Template blueprint tampering T9: Team assignment manipulation ⚠️ Public API Key VITE_SUPABASE_ANON_KEY exposed in client bundle Data Channels ● Auth (HTTPS) ● Realtime (WSS) ● Database (HTTPS) Legend HTTPS / Query Authentication WebSocket (Realtime) Trust Boundary (Untrusted) Trust Boundary (Trusted)
Admin (Session Creator)
Participant (Voter)
Database/Storage
Real-time Services
Authentication
Threat/Risk

System Components

Frontend (React SPA)

Component Technology Security Role
Admin Dashboard React 19, TypeScript Session management, question control, vote monitoring
Participant View React 19, TypeScript Vote submission, real-time updates
State Management Zustand Client-side state, no sensitive data persistence
Real-time Hooks Supabase JS Client WebSocket subscriptions, presence tracking
Password Gate sessionStorage Client-side only (bypassable)
Presentation View React 19, TypeScript Standalone projection window, no authentication required
Sequence Manager React 19, dnd-kit Unified slide/batch ordering
Image Uploader browser-image-compression Client-side image compression before Storage upload
Session Template Panel React 19, TypeScript Save/load session blueprints from Supabase
Team Components React 19, TypeScript Team picker, badges, filter tabs, QR grid — team assignment at vote time
Template Editor React 19, dnd-kit Visual session builder with drag-and-drop ordering

Backend (Supabase BaaS)

Service Purpose Security Controls
PostgreSQL Data persistence RLS policies, parameterized queries
Realtime WebSocket events, presence JWT authentication required
Auth Anonymous authentication JWT token generation
Storage Image file hosting RLS policies enforce path-based access for authenticated users

Data Flow Analysis

1. Session Creation

Admin → Create Session → DB Insert → Returns admin_token → Redirect to /admin/{token}

2. Participant Join

Scan QR/URL → /session/{id} → Anonymous Auth → Realtime Subscribe → Presence Track

3. Vote Submission

Participant Vote → Upsert to DB → Postgres Changes → Admin receives update

4. Admin Broadcast

Admin Action → DB Update → Broadcast Event → All Participants receive

5. Image Upload

Admin → Compress Image → Upload to Storage → Create session_item → Display in projection

6. Presentation Sync

Admin navigates → Broadcast event → Presentation window receives → Display updates

7. Team Assignment

Participant joins lobby → Selects team → team_id stored in Zustand → Sent with each vote upsert → Admin filters results by team

Trust Boundaries

🔴 Untrusted Zone: Client Browser
  • All client-side code can be inspected and modified
  • API keys (anon key) are publicly visible in the bundle
  • Client-side validation can be bypassed
  • Password gate is stored in sessionStorage (bypassable)
  • Admin token is exposed in URL (can be shared/leaked)
  • Presentation window is unauthenticated (read-only, receives broadcast events)
  • Image uploads are validated client-side only (type + size checks)
🟢 Trusted Zone: Supabase Backend
  • Row Level Security (RLS) enforces authorization
  • All database operations require valid JWT
  • Parameterized queries prevent SQL injection
  • TLS encryption for all communications

STRIDE Threat Analysis

Spoofing

S1: Admin Impersonation via Token Guessing HIGH

Admin tokens are UUIDs in URLs. If an attacker obtains or guesses the token, they gain full admin access to the session.

Existing Mitigation

UUIDs are cryptographically random (122 bits of entropy). Optional password gate adds a layer.

Recommended

Implement server-side admin authentication. Add rate limiting on session lookups.

S2: Participant Identity Manipulation MEDIUM

Anonymous auth allows anyone to create identities. A malicious user could create multiple identities to vote multiple times.

Existing Mitigation

Unique constraint on (question_id, participant_id) prevents duplicate votes per identity.

Recommended

Implement device fingerprinting or IP-based rate limiting for vote submission.

Tampering

T1: Vote Manipulation via Direct API Calls MEDIUM

Participants can modify their votes directly via API calls, bypassing the UI.

Existing Mitigation

RLS ensures participants can only modify their own votes. locked_in field could be used to prevent changes.

T2: Import File Tampering MEDIUM

Malicious JSON import files could contain XSS payloads or oversized data.

Existing Mitigation

Zod schema validation, 5MB file size limit, data sanitization on render.

T3: Session Template Blueprint Tampering LOW

Session templates store full session blueprints as JSONB in the database. A malicious user could craft API calls to insert oversized or malformed blueprints.

Existing Mitigation

Templates are serialized through the session-template-api which validates structure before storage. Blueprint data is re-validated on load. RLS policies restrict template creation to authenticated users.

T4: Team Assignment Manipulation LOW

Participants select their team client-side during lobby. A malicious user could modify the team_id sent with their vote via direct API calls, potentially skewing team-based results.

Existing Mitigation

Team assignment is a convenience feature for result filtering, not a security boundary. Votes are still uniquely constrained per participant. Team names are validated against the session's configured team list. The impact is limited to one participant appearing in the wrong team's results.

Repudiation

R1: Vote Attribution LOW

Anonymous voting by design means votes cannot be attributed to real identities.

Design Decision

This is an intentional feature. Votes are tied to anonymous auth.uid() for uniqueness enforcement only.

Information Disclosure

I1: Session Data Exposure HIGH

All authenticated users can SELECT from all tables. Session IDs are guessable nanoid strings. Vote reasons are exported in plain text.

Existing Mitigation

Session IDs use nanoid (21 characters). Export is admin-only action.

Recommended

Restrict SELECT queries to session participants only. Add session-level access control.

I2: API Key Exposure MEDIUM

Supabase anon key is publicly visible in the client bundle.

Design Decision

This is expected for Supabase public clients. Security relies on RLS, not key secrecy.

Denial of Service

D1: Large Import File Attack MEDIUM

Uploading large JSON files (up to 5MB) could slow down the client or cause memory issues.

Existing Mitigation

5MB file size limit. Client-side parsing with error handling.

D2: WebSocket Connection Flooding MEDIUM

Creating many WebSocket connections could exhaust Supabase realtime capacity.

Existing Mitigation

Supabase has built-in connection limits. Presence tracking helps monitor participants.

D3: Storage Bucket Abuse LOW

Authenticated users could upload many or large image files to the Supabase Storage bucket, consuming storage quota.

Existing Mitigation

Client-side image compression (browser-image-compression) reduces file sizes before upload. RLS policies restrict uploads to authenticated users with path-based access. Supabase plan storage quotas provide an upper bound.

Elevation of Privilege

E1: Participant to Admin Escalation HIGH

If a participant obtains the admin_token URL, they gain full admin privileges for that session.

Existing Mitigation

Admin token is a separate UUID. RLS checks created_by for mutation operations.

Recommended

Implement proper admin authentication. Use HTTP-only cookies instead of URL tokens.

E2: Client-Side Password Bypass MEDIUM

The optional password gate is client-side only. It can be bypassed by clearing sessionStorage or using dev tools.

Design Decision

This is a convenience feature, not a security control. True authorization is enforced by RLS.

Risk Summary Matrix

Threat ID Threat Likelihood Impact Risk Level Status
S1 Admin Token Guessing Low High HIGH Mitigated
S2 Multiple Vote Identities Medium Medium MEDIUM Mitigated
T1 Direct API Vote Manipulation Medium Low MEDIUM Accepted
T2 Malicious Import Files Low Medium MEDIUM Mitigated
I1 Session Data Exposure Medium Medium HIGH Partial
I2 API Key Exposure High Low MEDIUM By Design
D1 Large Import DoS Low Low LOW Mitigated
D2 WebSocket Flooding Low Medium MEDIUM Mitigated
E1 Participant to Admin Low High HIGH Partial
E2 Password Gate Bypass High Low MEDIUM By Design
T3 Template Blueprint Tampering Low Low LOW Mitigated
D3 Storage Bucket Abuse Low Low LOW Mitigated
T4 Team Assignment Manipulation Low Low LOW Accepted

Existing Security Controls

Control Type Description Effectiveness
Row Level Security (RLS) Authorization Database-level access control based on auth.uid() Strong
Anonymous JWT Auth Authentication Unique identity per browser session Moderate
HTTPS/TLS Transport Encryption in transit via Vercel & Supabase Strong
Zod Validation Input Validation Schema validation for imported data Strong
UUID Admin Tokens Access Control Cryptographically random session identifiers Moderate
Client Password Gate Access Control Optional password for admin routes Weak
Unique Vote Constraint Data Integrity One vote per participant per question Strong

Security Recommendations

High Priority

Medium Priority

Low Priority

Data Classification

Data Element Classification Storage Notes
Session ID Internal PostgreSQL Shared via QR code/URL
Admin Token Confidential PostgreSQL, URL Grants full session control
Vote Values Internal PostgreSQL Anonymous, aggregated for display
Vote Reasons Confidential PostgreSQL May contain personal opinions
Participant ID Public PostgreSQL Anonymous UUID, no PII
Question Text Internal PostgreSQL Admin-created content
Team Names Internal PostgreSQL (JSONB) Admin-configured, up to 5 per session
Team Assignment (vote) Public PostgreSQL Participant's self-selected team