Serverless Deployment and Security Hardening

By Mohamed Ali Krichen

FIREBASE REALTIME DATABASE NETLIFY CSP SECURITY AUDIT

This page documents the deployment of a real-time web application on a fully serverless infrastructure, and the three independent security audit cycles it went through before going public. The focus is on the infrastructure decisions, the attack surface they create, and the hardening applied at each layer.

Overview

The subject of this project is a real-time multiplayer web application deployed at bentwalad.netlify.app ↗. The technology choices were deliberately constrained: a single static HTML file, no server, no build pipeline, and no backend beyond what Firebase provides. This constraint is worth examining from a security standpoint because it removes the server-side trust layer that most web security models assume.

When there is no server, there is no place to validate requests before they reach the database. All business logic runs in the client. All database writes come directly from user browsers. All credentials are embedded in the source. This shifts the entire security responsibility onto two surfaces: the database access control rules, and the defensiveness of the client-side code. Both were found to have significant gaps on initial deployment and were hardened through three audit cycles.

Infrastructure components

Why this architecture is interesting to audit

A traditional web application has a server acting as a gatekeeper. Clients send requests, the server validates them, applies business logic, and writes to the database. Clients never touch the database directly. In a fully serverless client-side application, that gatekeeper does not exist. The database is publicly reachable, and the rules engine is the only access control layer. Auditing this infrastructure means auditing the rules themselves, the identity system backing them, and any client-side code that could be exploited to bypass them.

1. The Application

The deployed application is بنت / ولد (Bent / Walad), a real-time multiplayer word category game inspired by a classic Tunisian parlour game. Players join a shared room via a short code. One player picks a letter, and everyone races to fill in a set of word categories starting with that letter. The first player to submit ends the round. Players then vote on each other's answers, majority rules, and points go to uncontested responses.

The application supports both Latin and Arabic alphabet modes, accommodates up to 25 simultaneous players per room, and requires no account, no registration, and no installation. It is the vehicle chosen to demonstrate the deployment and security hardening documented on this page. The game itself is secondary to the infrastructure and security work it enabled.

SCOPE The application has user-generated content, shared mutable state, multiple concurrent writers, and public internet exposure. These properties create a meaningful and representative attack surface for a security audit cycle.

Application lobby screenshot:

بنت / ولد lobby interface

2. Architecture

The application communicates directly with Firebase using the Firebase Web SDK. There is no intermediate API layer, no proxy, and no server-side session management. Every read and write happens over an authenticated WebSocket connection to Firebase, evaluated in real time against the configured security rules.

Request flow from client browser to Firebase backend:

Client browser
  |
  +-- Netlify CDN  -->  delivers index.html (static)
  |
  +-- Google CDN  -->  Firebase SDK (loaded at runtime, no local bundle)
        |
        +-- Firebase Authentication
        |     signInAnonymously()  -->  Firebase issues signed JWT token
        |     token attached to all subsequent requests automatically
        |
        +-- Firebase Realtime Database
              reads  : onValue() WebSocket listener (server-push, no polling)
              writes : set() / update() / push() over authenticated connection
              rules  : evaluated server-side on every read and write attempt

Database write strategy

The application uses three distinct write operations. The distinction has direct security implications because Firebase rules are evaluated per path, and different operations touch different path scopes.

This write strategy is what makes per-player database scoping achievable at the rules layer. If all writes used set(), it would be impossible to permit a player to write their own data without also permitting them to overwrite all other data in the same document.

Identity model

Player identity is handled by Firebase Anonymous Authentication. On every page load, the client calls signInAnonymously(). Firebase issues a signed JWT without any user input. The token contains a unique uid that persists across page refreshes for the same browser session. This uid is referenced by security rules when evaluating per-player access control, and is cryptographically verifiable by Firebase's rules engine. A timeout fallback ensures the application still boots if Authentication is temporarily unreachable.

Data retention

Session data is stored only for the duration of an active game and deleted automatically when all players leave. Firebase's onDisconnect() API registers a server-side cleanup handler at connection time. If a browser closes without an explicit logout, Firebase executes the handler and removes the player's presence record. When the last presence entry is removed, the session data is deleted entirely from the database.

3. Deployment

The deployment stack is entirely free-tier. Netlify hosts the static file. Firebase provides the database and authentication backend. Neither service requires a paid plan at the usage levels of a publicly accessible application with moderate traffic.

Netlify static hosting

The application is deployed by dropping the HTML file and the netlify.toml configuration file onto the Netlify Deploys interface. Netlify serves the file from its global CDN with automatic HTTPS and full rollback history. The netlify.toml file applies HTTP security headers to every response.

// netlify.toml applied to all routes
[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self';
      script-src 'self' https://www.gstatic.com;
      connect-src https://*.firebaseio.com https://*.googleapis.com ...;
      font-src https://fonts.gstatic.com;
      img-src 'self' data:;
      style-src 'self' 'unsafe-inline' https://fonts.googleapis.com"
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"

Firebase setup requirements

Three steps are required in the Firebase Console before the application is safe for public access.

On embedded credentials

The Firebase configuration object including the API key is visible in the HTML source. Firebase API keys are client identifiers that specify which project to connect to. They carry no elevated privilege. Access to the database is governed entirely by the security rules, which require a valid Firebase-issued token on every request. An actor who has the API key but lacks a valid token will receive a permission-denied error on every database operation. This is discussed further under Scan 2 where it was formally assessed.

4. Security Scan 1

The security audits were performed by Claude (Anthropic) across three separate cycles. For each scan, the full application source code, the Firebase project configuration, the currently deployed database security rules, and the live application URL were provided. This gave the auditor visibility into both the intended behaviour and the actual enforced access controls at the database level.

The first audit was conducted on the initial deployed version. The database was running Firebase default test-mode rules. Four findings were identified.

Scan 1 findings:

Scan 1 findings table

Finding 1 : Open database

Test-mode rules permit any request to read or write any database path with no authentication requirement. The database URL is part of the public client configuration. Any party who loads the page and constructs a direct request to the Firebase endpoint can read all stored data, delete active sessions, or write arbitrary content without restriction.

Resolved. All read and write rules updated to require a valid Firebase-issued token. Requests without a valid token are rejected at rule evaluation before any data is accessed.

Finding 2 : Cross-site scripting via unsanitized innerHTML

User-provided strings were interpolated directly into HTML template literals and rendered via innerHTML with no sanitization. An attacker submitting a payload containing HTML markup or JavaScript event handler attributes as their display name or answer text will have that code execute in every other connected user's browser on the next render cycle. In a multi-user real-time application a single attacker can target all currently connected users simultaneously.

Resolved. A sanitizer function was applied to every user-provided string before any HTML insertion, replacing the five HTML-special characters with their entity equivalents, rendering injected markup inert.

// applied to every user-provided string rendered into the DOM
function esc(str) {
  return String(str || '')
    .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}

Finding 3 : No room ownership enforcement

Authentication establishes identity, not authorization. With only an authentication check on writes, any signed-in user can load any room document, modify its contents, and write it back using a full set() call. This includes resetting scores, manipulating application state, or removing other users from the session.

Resolved. The room creator's Firebase UID is stored in the room document at creation. The root write rule was updated to permit full document overwrites only from that UID. All other writes were refactored to targeted update() calls on specific sub-paths covered by dedicated scoped rules.

Finding 4 : Unvalidated state field values

The field governing application state transitions accepted any arbitrary string value. An authenticated user could write an unrecognised value to crash the client-side rendering logic for all connected users, or write a valid value out of sequence to skip application states.

Resolved. A database-level validation rule restricts the field to a whitelist of the four valid state values via regex match.

5. Security Scan 2

The second audit reviewed the post-remediation codebase and updated rules, using the same inputs as the first scan. All findings from Scan 1 were confirmed resolved. Five findings were raised, two of which required architectural changes beyond rule updates, and one was assessed as a design property rather than a vulnerability.

Scan 2 findings:

Scan 2 findings table

Finding 5 : Unscoped sub-path writes

The host-only root write rule blocks full document overwrites from non-hosts. However, Firebase evaluates the most specific matching rule for any given path. Sub-path writes using update() are governed only by rules at the specific target path. No sub-path rules existed, meaning any authenticated user could write to another player's data paths within a shared document.

Resolved. Explicit sub-path rules were added for all player-owned data paths, each scoped to the authenticated user's own identity. A user can only write to paths that contain their own identifier.

Finding 6 : Concurrent write race condition

The join flow read the current array length to compute the next index, then wrote the new entry at that index. Two clients joining within the same network window will both read the same length, compute the same index, and one write will silently overwrite the other. The affected user disappears from the session with no error.

Resolved. Join writes were replaced with Firebase's push() operation, which generates a server-side unique key in a single atomic operation. There is no client-side index computation and therefore no race window.

// before: read-compute-write, race condition possible
patch['players/' + currentLength] = { id: myId, name, score: 0 };
await update(roomRef, patch);
// after: server-side atomic key, no race window
await push(ref(db, 'rooms/' + code + '/players'), { id: myId, name, score: 0 });

Finding 7 : No HTTP security headers

The application was served without any HTTP security headers. Without a Content Security Policy the browser imposes no restrictions on which origins can execute scripts or make network requests. Without X-Frame-Options the page can be embedded in a third-party iframe for clickjacking. Without X-Content-Type-Options browsers may MIME-sniff responses and execute content in unintended contexts.

Resolved. A netlify.toml file was added to the deployment applying a Content Security Policy that whitelists only required origins, plus X-Frame-Options: DENY, X-Content-Type-Options: nosniff, and a strict Referrer-Policy on all responses.

Finding 8 : Insufficient room identifier entropy

Short room identifiers drawn from a small character set yield a combination space small enough that systematic enumeration of active rooms is feasible with automated requests at scale.

Resolved. Room identifiers were extended to increase the combination space by a factor of roughly one thousand, making systematic enumeration within any room's active window computationally impractical.

Finding 9 : API key embedded in source

The Firebase configuration object including the API key is visible in the page source. This was raised as an informational finding. Firebase API keys are public client identifiers by design. They specify which Firebase project to connect to and carry no privilege on their own. All access is gated by the security rules engine, which requires a valid Firebase-issued token regardless of whether the caller has the config object. Hiding the key would provide no security benefit in this model.

By Design. No remediation is possible or necessary. The rules layer is the correct access control boundary, not the key.

6. Security Scan 3

The third audit was conducted with the same inputs as previous scans. All prior remediations were confirmed correctly implemented. One finding was identified and formally assessed for risk acceptance.

Scan 3 findings:

Scan 3 findings table

Finding 10 : Client-side result computation

Scoring logic executes in the browser of the last user to submit their vote. That client reads the full vote dataset, performs the calculation, and writes the results back to the database. A user with browser developer tools access could intercept this write before it executes and modify the computed values in memory. The security rules permit this write because it originates from an authenticated user at a path that must be writable to complete the application flow.

The correct remediation is a server-side Cloud Function executing in a trusted environment outside client control. This requires a paid infrastructure tier.

Accepted. The risk is accepted given that manipulation would be immediately visible to other participants, the raw vote data stored in the database would contradict any manipulated score, and the social deterrent in a community context is meaningful. The finding is documented as a known limitation with a defined remediation path for a future iteration.

ACCEPTED RISK Client-side result computation is a structural limitation of the current billing tier. Moving the computation to a server-side function resolves this finding by removing the calculation from any surface accessible to client manipulation.

Conclusion

A fully serverless client-side architecture is viable for production deployment on the public internet. The initial deployment demonstrated what the default posture looks like when that architecture is not hardened: an open database, no identity verification, no ownership model, no input validation, and no browser-level containment. Three audit cycles produced a deployment where every read requires a cryptographically verified identity, every write is scoped to the authenticated user's own data paths, client-rendered output is sanitized against injection, the HTTP response surface is constrained by policy, and the session identifier space resists enumeration.

The broader pattern this project illustrates is that serverless does not mean lower attack surface. It means differently shaped attack surface. The absence of a server relocates access control from server-side code to database rules and client-side defences. Both require the same systematic approach.

Risk acceptance

Risk acceptance is a formal decision to tolerate a known finding because the cost or complexity of remediation outweighs the realistic impact in the current context. In this project, the client-side computation finding was accepted: remediating it requires a paid infrastructure tier, the impact is bounded and detectable, and the social context provides a meaningful deterrent. Risk acceptance is not the same as ignoring a finding. The finding is documented, the residual risk is understood, and a remediation path exists and is tracked.

Assurance through iterative scanning

Assurance means providing confidence that controls are working as intended, not just that they were implemented. Each scan cycle served this function: the auditor was given the live deployed rules and the application URL, not just the source code. This means each review covered the actual production posture. Confirming that previous remediations held across subsequent scans is itself an assurance activity. A fix that exists in source but was never deployed provides no assurance.

Remediation verification

Remediation is only effective when it is verified. The three-cycle structure of this audit, where each scan explicitly re-checked prior findings before raising new ones, reflects this principle. Scans 2 and 3 each opened by confirming all previous findings were resolved before proceeding. This prevents the common failure mode where fixes are applied in one place but bypass the actual attack path.

Final posture

All findings across three scan cycles:

Full findings recap table