~/blog/defending-a-static-spaPOSTED
by Shady Nathan Tawfik · May 16, 2026 · 4 min

Eight Attack Vectors, Zero Findings: What A Hardened Single Page Application Looks Like

A negative assessment is a result. This one documents why eight standard vectors failed against a statically served application behind an edge platform.

Interactive[blog-2.0]

TL;DR: A negative assessment is a result. I ran eight standard attack vectors against a static single page application and documented every failure, then turned the list into a regression gate that runs on every build.

The scenario

I ran this as a black-box review: no credentials, only the publicly visible surface. Here is what I saw before I touched anything.

A data platform delivered as a pre-rendered single page application on a static host with an edge runtime in front. All eight planned vectors were executed and recorded, including path traversal, server-side template injection, API abuse, storage exposure and authorisation bypass.

I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.

The pattern

The pattern I recognised — and that I keep finding in similar estates:

Most published web application findings assume a server that renders something. That assumption is what makes them cheap: inject here, execute there, read the response. Remove the server-side renderer and the majority of these techniques have nothing to attach to.

A static build changes the shape of the problem rather than removing it. There is no runtime to inject into, which removes server-side template injection, deserialisation and most injection classes outright. Attack surface reduces to the edge configuration, the client bundle and whatever API the bundle talks to. That last part is where the real work is, and it is where a static site either holds or quietly becomes a thin client for an unprotected backend.

Recording the failures matters as much as recording the successes. A negative assessment is the only way to justify keeping an architecture that a later reviewer will otherwise question, and the recorded vector list becomes a regression suite: the same eight tests, rerun after every deployment, show whether a change opened a door.

TL;DR: A negative assessment is a result. This one documents why eight standard vectors failed against a statically served application behind an edge platform.

Findings

SeverityFindingEvidence
INFOPath traversal not reachableStatic routing returns the same document for every path
INFOServer-side template injection impossibleNo server-side rendering in the request path
INFOAdministrative routes not servedUnpublished routes absent from the build output
INFOStorage buckets not publicMedia served through signed, expiring URLs
INFOAuthorisation enforced in the data layerDirect object access tested against foreign identifiers
INFOEdge normalised host and pathHost header poisoning and path confusion rejected

The attack path

rejectsAttack requestEdge platformStatic document storePre-rendered pageSigned API callAuthorisation in the data layerAuthorised data or refusalTraversal, injection, host tricks

Reproduction in my lab

Every command below targets a lab container I control. Nothing here is aimed at a live system.

Run the regression suite against your own static deployment

~/code/bash bash
# target: your own static site on localhost
$ python3 -m http.server 8080 --directory dist

# 1. traversal and confusion
$ for p in '/../etc/passwd' '/%2e%2e/%2e%2e/etc/passwd' '/index.html?x=../../secret'; do
    printf '%s -> %s\n' "$p" "$(curl -so /dev/null -w '%{http_code}' "http://localhost:8080$p")"
  done

# 2. unpublished routes must not resolve to content
$ for p in /admin /api/admin /internal; do
    printf '%s -> %s\n' "$p" "$(curl -so /dev/null -w '%{http_code}' "http://localhost:8080$p")"
  done

Detection in your own estate

Compare the published route list with the build output

~/code/bash bash
# what does the build actually contain
$ ls dist/assets | head

# and what does the edge serve for paths that are not in it
$ for p in /admin /api/admin /.env /config.json; do
    printf '%s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://example.org$p)"
  done

Check the API authorisation from the outside

~/code/bash bash
# use an object you own, then swap only the identifier
$ curl -s -o /dev/null -w '%{http_code}\n' 'https://api.example.org/v1/items/own-id'
$ curl -s -o /dev/null -w '%{http_code}\n' 'https://api.example.org/v1/items/other-id'

Remediation

Keep the negative result as a regression gate in the pipeline

~/code/yaml yaml
# run the static regression checks on every build
name: static-surface
on: [push, pull_request]
jobs:
  surface:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - name: unpublished routes must not resolve
        run: |
          for p in /admin /api/admin /.env /config.json; do
            code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:8080$p")
            test "$code" = "404" || { echo "leaked route: $p ($code)"; exit 1; }
          done

Takeaways

  • A negative assessment is a deliverable, not an absence of one.
  • Static delivery removes injection classes, not authorisation.
  • Record the vectors you proved fail, then rerun them forever.
  • The bundle and the API are the whole attack surface once the server is gone.

For educational purposes only. Every reproducible command targets my own lab environment (localhost), never a live system. © 2026