🔍 The Modern SPA Testing Paradox

For more than two decades, the standard operating procedure for web application security assessments has centered around the interception proxy. Tools like Burp Suite, OWASP ZAP, and Caido position themselves between the browser and the target origin, capturing every HTTP request and response traversing the network socket.

In traditional server-rendered architectures (PHP, ASP.NET, Rails), this model was nearly exhaustive: every routing decision, session transition, and authorization challenge occurred strictly across the wire.

However, the rapid migration to modern Single-Page Applications (SPAs) and hybrid frameworks—powered by React, Next.js, Remix, Vue 3, Pinia, and Angular—has introduced a fundamental architectural blindspot into this workflow.

Modern SPAs do not merely display data received from an API; they maintain dynamic, complex, in-memory state machines that execute entirely within the client’s browser runtime:

Traditional SSR Paradigm:
[Browser UI] <================= (HTTP Wire / Proxy Visible) =================> [Backend Controller / DB]

Modern SPA Architecture:
[Browser UI] <---> [In-Memory State / Route Chunks / Sinks] <===(HTTP)===> [API Gateway / Microservices]
                          ▲
                          └── [THE APPSEC BLINDSPOT: Invisible to Network Proxies]

The Three Blindspots of Network Interception

  1. Unlinked Route Chunk Manifests: When modern web bundlers (Webpack, Turbopack, Vite) build a production application with code-splitting, internal and administrative route definitions (e.g., /admin/tenant-provisioning, /internal/feature-flags) are compiled into separate JavaScript chunks. If a standard user has no navigational button pointing to these routes, they never emit an HTTP request during standard crawling. A network proxy sees zero bytes of traffic for these endpoints, yet their route signatures and parameter contracts sit in plain view within client-side memory.
  2. Ephemeral In-Memory State & Secrets: Modern frontends heavily utilize reactive state stores (Redux, Pinia, Zustand, Vuex) and global window namespaces. Temporary bearer tokens, unmasked user identifiers, internal API gateway keys, and multi-tenant metadata frequently persist inside client memory closures without being explicitly stored in localStorage or document.cookie.
  3. Cross-Origin Message Sinks (postMessage): Single-page applications embedded in iframes or communicating with third-party authentication providers rely on window.postMessage. If an event listener processes inbound payloads without strict event.origin validation, it creates severe DOM-based Cross-Site Scripting (DOM XSS) and state poisoning vectors (CWE-345) that never touch the network layer.

Conversely, once an ethical security researcher or internal AppSec engineer identifies an undocumented endpoint or sensitive parameter, the opposite failure mode emerges: scope drift and compliance liability. Testing beyond authorized domain boundaries, hitting cloud metadata endpoints (169.254.169.254), or attaching unredacted Personally Identifiable Information (PII) to a bug bounty report exposes researchers to legal jeopardy and program disqualification.

To close this gap from discovery through verification, we developed and open-sourced a coordinated two-stage AppSec pipeline: StateHunter (in-browser client reconnaissance) and AuditGuard (terminal boundary enforcement and Safe Harbor certification).


🎯 Phase 1: In-Browser SPA State Reconnaissance with StateHunter

StateHunter is a high-performance Chrome DevTools extension architected under Manifest V3. Rather than injecting bulky, intrusive third-party scripts into the host page context, StateHunter runs within an isolated DevTools inspection bridge to passively introspect, parse, and categorize client-side JavaScript execution in real time.

flowchart LR
    subgraph Browser ["Chrome Browser Context (Target App)"]
        DOM["DOM & Window Object"]
        Chunks["JS Chunks & Router Manifests"]
        MsgHandler["window.addEventListener('message')"]
        Stores["Reactive State (Redux / Pinia / Storage)"]
    end

    subgraph StateHunter ["StateHunter (Manifest V3 DevTools Panel)"]
        Inspector["Runtime Introspector"]
        ChunkParser["De-Obfuscation Engine"]
        SinkMonitor["postMessage Sink Auditor"]
        SecretRegex["High-Entropy Token Filter"]
        YAMLGen["Scope YAML Exporter"]
    end

    DOM --> Inspector
    Chunks --> ChunkParser
    MsgHandler --> SinkMonitor
    Stores --> SecretRegex

    ChunkParser --> YAMLGen
    SinkMonitor --> YAMLGen
    SecretRegex --> YAMLGen
    YAMLGen -->|scope.yaml| AuditGuardEngine["AuditGuard Terminal Engine"]

1. De-Obfuscating Unlinked Routes from Production Chunks

In modern frameworks like Next.js (App Router or Pages Router), route manifests are embedded across minified JavaScript chunks. StateHunter traverses loaded script tags and AST manifests to extract declared path strings, regular expression parameters, and dynamic slug masks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Sample de-obfuscation logic executing within StateHunter's runtime inspector:
export function extractUnlinkedRoutes(source: string): string[] {
  const routePatterns = [
    // Next.js App Router dynamic route definitions
    /["'](?:\/[a-zA-Z0-9_-]+)+(\/\[[a-zA-Z0-9_-]+\])*["']/g,
    // Standard client-side routing tables (React Router / Vue Router)
    /path\s*:\s*["'](\/[^"']+)["']/g,
    // Endpoint contracts in compiled Axios / Fetch wrappers
    /(?:get|post|put|delete|patch)\s*\(\s*["'](\/api\/v[0-9]+\/[^"']+)["']/gi
  ];

  const discovered = new Set<string>();
  for (const regex of routePatterns) {
    let match: RegExpExecArray | null;
    while ((match = regex.exec(source)) !== null) {
      const candidate = match[1] || match[0].replace(/["']/g, '');
      if (isValidRoute(candidate)) {
        discovered.add(candidate);
      }
    }
  }
  return Array.from(discovered).sort();
}

When activated on a target application, StateHunter instantly maps endpoints that do not appear in any visible navigation menu, such as internal administrative routes (/api/v2/internal/tenants/:id/diagnostics) or unlinked debug consoles.

2. Auditing Runtime postMessage Handlers

Cross-document messaging vulnerabilities arise when applications register message event listeners without verifying the origin of the calling frame. StateHunter hooks window.addEventListener('message') via an isolated instrumentation shim, recording incoming message signatures and analyzing the handler’s execution graph:

[!WARNING] If a handler uses wildcard target origins ("*") or executes eval() or element.innerHTML = event.data without strict schema sanitization, StateHunter flags the sink as a High-Risk DOM Poisoning Sink and captures the stack trace.

3. Deep Memory State & High-Entropy Key Extraction

Beyond routing, StateHunter recursively inspects accessible global window namespaces, sessionStorage keys, and reactive state stores. It applies Shannon entropy analysis alongside targeted regular expressions to flag:

  • Leaked JSON Web Tokens (eyJ...)
  • Cloud provider access keys (AKIA..., AIza...)
  • Private tenant IDs and internal UUIDv4 identifiers

4. Direct Scope Export (scope.yaml)

Rather than forcing the tester to copy-paste findings into arbitrary notes, StateHunter compiles all discovered hosts, categorized endpoints, and sensitive parameters into an AuditGuard-compatible YAML manifest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Generated by StateHunter v1.0.0
program_id: "acuity-health-portal"
in_scope_domains:
  - "portal.acuityhealth.local"
  - "api.acuityhealth.local"
discovered_routes:
  - path: "/api/v1/patients/{patientId}/records"
    auth_required: true
    source: "chunk-7024.js"
  - path: "/api/v1/admin/audit-logs"
    auth_required: true
    is_admin_candidate: true
    source: "chunk-3189.js"
flagged_parameters:
  - name: "patientId"
    type: "UUIDv4"
    risk: "IDOR_CANDIDATE"

🛡️ Phase 2: Mathematical Boundary Enforcement & Verification with AuditGuard

Finding an unlinked route or an undocumented parameter is only the first step. Conducting automated verification against that route without strict guardrails introduces substantial risks:

  • Exceeding the authorized program scope (testing third-party SSO or cloud provider endpoints).
  • Triggering unintended Denial-of-Service (DoS) via unthrottled concurrent requests.
  • Exposing unauthorized sensitive data in public reports without cryptographic proof of compliance.

AuditGuard is a pure-Python, zero-dependency command-line framework built to solve these boundary problems through mathematical rigor.

flowchart TD
    subgraph Ingest [1. Ingestion & Scope Validation]
        SHYaml[StateHunter scope.yaml] --> CLI[AuditGuard CLI]
        CLI --> ScopeEngine[Deterministic Scope Engine]
        ScopeEngine -->|CIDR / Wildcard Filter| ScopeDecision{Target In-Scope?}
        ScopeDecision -->|NO: Prohibited| AbortReq[🚨 Immediate Request Drop & Alert]
    end

    subgraph Execution [2. Safe Testing & Diagnostics]
        ScopeDecision -->|YES: Validated| Bucket[Token-Bucket Rate Limiter]
        Bucket --> Prober[Dual-Role IDOR Prober]
        Bucket --> NucleiRunner[Pure-Python Nuclei Engine]
        Prober --> EvidenceLog[(Audit Ledger: audit_log.jsonl)]
        NucleiRunner --> EvidenceLog
    end

    subgraph Certification [3. Scoring & Legal Protection]
        EvidenceLog --> CVSS[FIRST.org CVSS v3.1 Calculator]
        EvidenceLog --> SafeHarbor[Disclose.io Proof-of-Adherence Engine]
        SafeHarbor --> SOWReport[Cryptographic SOW Certificate]
        CVSS --> PlatformExport[1-Click Triage Exporters: H1 / Bugcrowd / Jira]
    end

1. Deterministic Scope Enforcement

AuditGuard does not rely on simple substring matching. It parses program scopes into mathematical boundary trees using standard CIDR blocks, wildcard domain hierarchies, and strict URL path exclusions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# AuditGuard's zero-dependency scope validation logic:
import ipaddress
import re
from urllib.parse import urlparse

class ScopeValidator:
    def __init__(self, in_scope_targets: list[str], prohibited_subnets: list[str] = None):
        self.domains = [t.lower() for t in in_scope_targets if not self._is_ip(t)]
        self.subnets = [ipaddress.ip_network(t) for t in in_scope_targets if self._is_ip(t)]
        # Default safety: prohibit cloud metadata & loopback
        self.blacklisted_nets = [
            ipaddress.ip_network("169.254.169.254/32"), # Cloud metadata
            ipaddress.ip_network("127.0.0.0/8"),         # Localhost loopback
        ]

    def is_authorized(self, url: str) -> tuple[bool, str]:
        parsed = urlparse(url)
        hostname = parsed.hostname or ""

        # Check IP boundaries
        try:
            ip_obj = ipaddress.ip_address(hostname)
            for net in self.blacklisted_nets:
                if ip_obj in net:
                    return False, f"Prohibited cloud metadata/internal address: {ip_obj}"
            for allowed in self.subnets:
                if ip_obj in allowed:
                    return True, "Authorized via CIDR match"
            return False, f"IP {ip_obj} is outside authorized CIDR subnets"
        except ValueError:
            pass # Domain name string

        # Check domain hierarchy
        for domain in self.domains:
            if domain.startswith("*."):
                suffix = domain[2:]
                if hostname == suffix or hostname.endswith("." + suffix):
                    return True, f"Authorized via wildcard match: {domain}"
            elif hostname == domain:
                return True, "Authorized via exact domain match"

        return False, f"Domain '{hostname}' is not authorized under program scope"

If an HTTP request targets a host outside the authorized boundary tree or attempts to touch the link-local metadata address (169.254.169.254), AuditGuard drops the socket connection instantly and appends an anomaly event to the tamper-evident audit ledger.

2. Dual-Role Authorization Matrix & IDOR / BOLA Probing

Broken Object Level Authorization (OWASP API1 / CWE-639) is among the most prevalent flaws in modern microservices. When StateHunter identifies dynamic route parameters (e.g. /api/v1/patients/{patientId}/records), AuditGuard automates safe, dual-role access verification.

By supplying credentials for two distinct researcher-controlled accounts (user_a and user_b):

  1. AuditGuard fetches a valid resource belonging to user_a.
  2. It re-executes the exact request using user_b’s session token.
  3. It performs status code, response length, and AST content differential analysis.
sequenceDiagram
    autonumber
    participant AG as AuditGuard Engine
    participant API as Target API Gateway
    participant DB as Backend Datastore

    Note over AG,API: Dual-Role Comparative Matrix
    AG->>API: GET /api/v1/patients/UUID-A/records (Token User A)
    API->>DB: Query Tenant A
    DB-->>API: 200 OK (User A Medical Data)
    API-->>AG: 200 OK [Length: 1,420 bytes]

    AG->>API: GET /api/v1/patients/UUID-A/records (Token User B)
    alt Flaw Present (IDOR / BOLA)
        API->>DB: Query Tenant A (Missing Tenancy Filter!)
        DB-->>API: 200 OK (User A Data Leaked to User B)
        API-->>AG: 200 OK [Length: 1,420 bytes]
        Note over AG: 🚨 IDOR Confirmed! Flagged as High Severity
    else Properly Hardened (Role-Enforced)
        API->>DB: Check Context Ownership
        API-->>AG: 403 Forbidden / 404 Not Found
        Note over AG: ✅ Authorization Boundary Secure
    end

3. Declarative Nuclei-Compatible YAML Diagnostics (Pure Python)

Rather than forcing users to bundle external Go runtime binaries, AuditGuard includes a native Python interpreter for declarative ProjectDiscovery Nuclei-compatible HTTP YAML templates.

It executes read-only diagnostic templates—verifying security headers, checking for CORS misconfigurations, and validating cache control policies—without external dependencies:

1
2
# Execute local Nuclei YAML checks strictly against authorized in-scope routes:
auditguard scan --program acuity-health --template security-headers.yaml

4. Deterministic FIRST.org CVSS v3.1 Scoring

Hallucinated or inflated vulnerability severities damage credibility during triage. AuditGuard implements the official FIRST.org CVSS v3.1 metric specification entirely in pure Python:

1
2
3
4
# Pure-Python CVSS v3.1 calculation
vector = "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"
score, severity = AuditGuardCVSS.calculate(vector)
# Returns: Base Score: 6.5 | Severity: MEDIUM

Every exported report includes the verifiable vector equation, calculated exploitability subscore, and impact metrics.

5. Cryptographic Safe Harbor Defense Certificates

Under the Disclose.io Core Vulnerability Disclosure Standard, ethical researchers who adhere strictly to program rules are afforded legal Safe Harbor protections against CFAA (18 U.S.C. § 1030) and DMCA (§ 1201) anti-circumvention claims.

AuditGuard writes every outbound request, timestamp, scope evaluation, and rate-limit throttle event into an append-only JSONL ledger (audit/audit_log.jsonl). When testing concludes, AuditGuard evaluates the log and produces a signed Proof-of-Adherence Certificate:

================================================================================
                    DISCLOSE.IO SAFE HARBOR ADHERENCE CERTIFICATE
================================================================================
Program Identifier:      acuity-health-portal
Assessment Start:        2026-09-12 09:15:00 UTC
Assessment Conclusion:   2026-09-12 11:30:00 UTC
Total Requests Emitted:  184 HTTP transactions
Average Request Rate:    1.2 req/sec (Ceiling: 2.0 req/sec)
Out-of-Scope Attempts:   0 (100% boundary compliance)
Prohibited Route Drops:  0
Ledger SHA-256 Digest:   e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

LEGAL ATTESTATION:
All actions recorded in the referenced ledger strictly complied with the authorized
boundaries, testing constraints, and non-destructive protocols of the target VDP.
================================================================================

🚀 The Complete Recon-to-Report Workflow

Here is how the end-to-end testing pipeline operates in practice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Step 1: In Chrome, use StateHunter DevTools to inspect target SPA and export scope
# Result: statehunter_acuity_scope.yaml generated in your downloads folder

# Step 2: Import the passive scope into AuditGuard
auditguard import-scope statehunter_acuity_scope.yaml --program acuity-portal

# Step 3: Run boundary and route diagnostics
auditguard audit --program acuity-portal --pace 1.5

# Step 4: Test flagged IDOR candidates using dual credentials
auditguard probe-idor --program acuity-portal \
  --route "/api/v1/patients/{patientId}/records" \
  --user-a-token "ey..." \
  --user-b-token "ey..."

# Step 5: Export verified findings directly to HackerOne or Bugcrowd
auditguard export --program acuity-portal --format hackerone --output report.md

📢 Call for Peer Review & Community RFC

Open-source security engineering relies on adversarial peer review and community scrutiny. We are publishing StateHunter and AuditGuard not just as finished tools, but as an open research framework for client-to-boundary application security.

We are actively seeking feedback from the application security community, penetration testers, bug bounty hunters, and academic researchers:

Key Areas for Community Feedback:

  1. AST & Route Chunk De-obfuscation:
    • What edge cases exist in production Vite, Turbopack, or Webpack bundle chunking that evade regex-based route detection?
    • How can we improve tree-shaking heuristic analysis in StateHunter without executing untrusted client code?
  2. Deterministic Scope Boundaries:
    • Are there complex cloud networking topologies (e.g. multi-region AWS API Gateway custom domain mappings, Cloudflare Workers routes) where pure CIDR/regex boundary checks yield false negatives?
  3. Safe Harbor Cryptographic Ledger:
    • How can the audit ledger be enhanced to support verifiable zero-knowledge or HMAC-based multi-party timestamping, further bolstering legal non-repudiation during disputed triage?
  4. Dual-Role Prober Differential Heuristics:
    • What statistical or AST heuristics best eliminate false positives when testing dynamic responses with variable timestamps or CSRF tokens?

How to Participate:

  • 🛡️ StateHunter Repository: github.com/SixFiveMil/statehunter
    • Test the extension in Chrome DevTools against real-world SPAs.
    • Submit issues, route de-obfuscation test fixtures, and PRs.
  • ⚖️ AuditGuard Repository: github.com/SixFiveMil/auditguard
    • Run the test suite: pytest tests/ (89 tests, 100% passing).
    • Review docs/ARCHITECTURE.md and docs/SAFE_HARBOR_LEGAL.md.
  • 💬 Discussion & Feedback:
    • Open an issue or join the discussion on GitHub.
    • Share feedback on Reddit (r/netsec, r/bugbounty) and Hacker News.

Both tools are released under the open-source MIT License, transmit zero telemetry, and execute 100% locally.