Playwright XSS Verification

Playwright XSS Verification

The following files were used as context for generating this wiki page:

Overview

This page documents WSHawk's Playwright-based browser verification system for Cross-Site Scripting (XSS) vulnerabilities. The HeadlessBrowserXSSVerifier class uses Playwright to load responses in a real Chromium browser and verify that injected JavaScript actually executes, providing definitive proof of exploitability beyond simple pattern matching.

For information about the initial XSS detection logic that triggers browser verification, see page 4.1 (Vulnerability Detection Modules). For out-of-band testing of blind vulnerabilities, see page 4.4 (OAST Blind Vulnerability Detection).


The False Positive Problem

Traditional XSS scanners rely on payload reflection: if an injected payload appears in the response, the scanner reports a vulnerability. This approach produces false positives because:

  • HTML encoding: <script>alert(1)</script> may be reflected as &lt;script&gt;alert(1)&lt;/script&gt;
  • JavaScript escaping: Payload injected into a string context may be properly escaped
  • Content Security Policy: Browser may block inline script execution
  • Non-executable contexts: Payload reflected inside <textarea>, HTML comments, or attribute values that don't execute JavaScript

WSHawk's browser verification solves this by loading the response in a real browser and confirming script execution.

| Detection Method | What It Confirms | False Positive Rate | |------------------|------------------|---------------------| | Pattern Matching | Payload reflected in response | High (30-50%) | | Context Analysis | Payload in potentially executable context | Medium (10-20%) | | Browser Verification | JavaScript actually executed | Very Low (<5%) |

Sources: scanner_v2.py:212-290, README.md:28-32


System Architecture

Component Overview

The Playwright verification system consists of three main components:

graph TB
    subgraph "WSHawkV2 Scanner (scanner_v2.py)"
        Scanner["WSHawkV2"]
        UseFlag["use_headless_browser: bool"]
        VerifierRef["headless_verifier: HeadlessBrowserXSSVerifier"]
        TestXSS["test_xss_v2()"]
    end
    
    subgraph "Initial Detection (vulnerability_verifier.py)"
        VulnVerifier["VulnerabilityVerifier"]
        VerifyXSS["verify_xss(response, payload)"]
        ReturnConf["Returns (is_vuln, confidence, description)"]
    end
    
    subgraph "Browser Verification (headless_xss_verifier.py)"
        HeadlessVerifier["HeadlessBrowserXSSVerifier"]
        Browser["Chromium browser instance"]
        StartMethod["start()"]
        VerifyMethod["verify_xss_execution(response, payload)"]
        StopMethod["stop()"]
    end
    
    Scanner --> UseFlag
    Scanner --> VerifierRef
    Scanner --> TestXSS
    
    TestXSS --> VulnVerifier
    VulnVerifier --> VerifyXSS
    VerifyXSS --> ReturnConf
    
    ReturnConf -->|"ConfidenceLevel.HIGH"| HeadlessVerifier
    HeadlessVerifier --> Browser
    HeadlessVerifier --> StartMethod
    HeadlessVerifier --> VerifyMethod
    HeadlessVerifier --> StopMethod
    
    VerifyMethod -->|"(is_executed, evidence)"| Scanner

Sources: scanner_v2.py:28-75, scanner_v2.py:52-54


WSHawkV2 Scanner Integration

The WSHawkV2 class initializes browser verification support during construction:

| Attribute | Type | Purpose | Default Value | |-----------|------|---------|---------------| | use_headless_browser | bool | Enable/disable browser verification | True | | headless_verifier | HeadlessBrowserXSSVerifier | Browser verifier instance | None (lazy init) |

Initialization:

scanner_v2.py:52-54 sets default values for browser verification flags. The headless_verifier is initialized lazily on first use to avoid launching Chromium unnecessarily.

Sources: scanner_v2.py:52-54


XSS Testing Workflow

test_xss_v2() Method

The test_xss_v2() method in WSHawkV2 coordinates XSS testing with optional browser verification:

Workflow Diagram:

flowchart TD
    Start["test_xss_v2(ws)"]
    LoadPayloads["Load XSS payloads from WSPayloads.get_xss()"]
    GetBase["Get base message from sample_messages"]
    
    LoopStart{"For each payload"}
    InjectPayload["Inject payload into message structure"]
    SendMsg["ws.send(injected_message)"]
    RecvResp["ws.recv() - Wait for response"]
    
    PatternCheck["VulnerabilityVerifier.verify_xss(response, payload)"]
    ConfidenceCheck{"confidence == HIGH?"}
    BrowserEnabledCheck{"use_headless_browser == True?"}
    
    InitBrowser["Initialize HeadlessBrowserXSSVerifier"]
    BrowserExists{"headless_verifier exists?"}
    StartBrowser["headless_verifier.start()"]
    
    BrowserVerify["headless_verifier.verify_xss_execution(response, payload)"]
    ExecutedCheck{"is_executed == True?"}
    
    UpgradeConf["confidence = ConfidenceLevel.CRITICAL"]
    SetFlag["browser_verified = True"]
    UpdateDesc["description = 'REAL EXECUTION: {evidence}'"]
    
    AddVuln["Add to vulnerabilities list with browser_verified flag"]
    NextPayload["Next payload"]
    End["Return results"]
    
    Start --> LoadPayloads
    LoadPayloads --> GetBase
    GetBase --> LoopStart
    
    LoopStart -->|Yes| InjectPayload
    InjectPayload --> SendMsg
    SendMsg --> RecvResp
    RecvResp --> PatternCheck
    
    PatternCheck --> ConfidenceCheck
    ConfidenceCheck -->|Yes| BrowserEnabledCheck
    ConfidenceCheck -->|No| AddVuln
    
    BrowserEnabledCheck -->|Yes| BrowserExists
    BrowserEnabledCheck -->|No| AddVuln
    
    BrowserExists -->|No| InitBrowser
    BrowserExists -->|Yes| BrowserVerify
    InitBrowser --> StartBrowser
    StartBrowser --> BrowserVerify
    
    BrowserVerify --> ExecutedCheck
    ExecutedCheck -->|Yes| UpgradeConf
    ExecutedCheck -->|No| AddVuln
    
    UpgradeConf --> SetFlag
    SetFlag --> UpdateDesc
    UpdateDesc --> AddVuln
    
    AddVuln --> NextPayload
    NextPayload --> LoopStart
    LoopStart -->|No more| End

Sources: scanner_v2.py:212-290


Code Flow Details

Payload Injection:

scanner_v2.py:224-230 uses MessageAnalyzer to inject payloads into the learned message structure. If the learning phase detected JSON format, payloads are injected into multiple fields automatically.

Initial Pattern Detection:

scanner_v2.py:240-243 calls VulnerabilityVerifier.verify_xss() which performs context-aware pattern matching to assign an initial confidence level (LOW, MEDIUM, or HIGH).

Browser Verification Trigger:

scanner_v2.py:247-263 checks two conditions before browser verification:

  1. confidence == ConfidenceLevel.HIGH - Only verify high-confidence candidates
  2. self.use_headless_browser == True - Browser verification is enabled

Lazy Browser Initialization:

scanner_v2.py:250-252 creates the HeadlessBrowserXSSVerifier instance on first use and calls start() to launch the Chromium browser. Subsequent XSS tests reuse the same browser instance.

Verification Execution:

scanner_v2.py:254-256 calls verify_xss_execution() which loads the response HTML in the browser and waits for JavaScript execution events.

Confidence Escalation:

scanner_v2.py:258-261 upgrades the confidence to CRITICAL when browser verification confirms execution, proving the vulnerability is exploitable in a real browser.

Sources: scanner_v2.py:212-290


HeadlessBrowserXSSVerifier Class

Class Responsibilities

The HeadlessBrowserXSSVerifier class (referenced at scanner_v2.py:21, scanner_v2.py:54) encapsulates all Playwright browser operations:

| Method | Purpose | Return Type | |--------|---------|-------------| | start() | Launch Chromium browser | None | | verify_xss_execution(response, payload) | Load HTML and verify script execution | (bool, str) | | stop() | Shutdown browser and cleanup | None |

Browser Lifecycle:

stateDiagram-v2
    [*] --> Uninitialized: headless_verifier = None
    Uninitialized --> Starting: HeadlessBrowserXSSVerifier()
    Starting --> Running: start() - Launch Chromium
    Running --> Verifying: verify_xss_execution()
    Verifying --> Running: Return (is_executed, evidence)
    Running --> Stopping: stop()
    Stopping --> [*]: Browser closed
    
    note right of Running
        Browser instance reused
        across multiple XSS tests
    end note

Sources: scanner_v2.py:21, scanner_v2.py:250-256


verify_xss_execution() Method

This method performs the actual browser-based verification:

Verification Process:

  1. Create Browser Page: Opens new browser page in existing Chromium instance
  2. Set HTML Content: Loads the WebSocket response as HTML using page.set_content()
  3. Monitor Console Events: Attaches listener to capture JavaScript console.log() calls
  4. Wait for Execution: Waits up to 2 seconds for script execution indicators
  5. Capture Screenshot: Takes screenshot as visual proof of vulnerability
  6. Return Evidence: Returns (is_executed: bool, evidence: str) tuple

Execution Indicators:

The method detects script execution through multiple signals:

  • console.log(), console.error(), or console.warn() events
  • alert() dialog interception
  • DOM mutations indicating script activity
  • JavaScript errors in console

Sources: scanner_v2.py:254-256


Vulnerability Storage

Evidence Fields

When browser verification succeeds, the vulnerability dictionary includes additional fields:

| Field | Type | Purpose | Example Value | |-------|------|---------|---------------| | type | str | Vulnerability classification | "Cross-Site Scripting (XSS)" | | severity | str | CVSS-based severity | "CRITICAL" | | confidence | str | Confidence level | "CRITICAL" (upgraded from HIGH) | | description | str | Vulnerability description | "REAL EXECUTION: script executed in browser" | | payload | str | Injected XSS payload | "<script>alert(1)</script>" | | response_snippet | str | First 200 chars of response | Truncated response | | browser_verified | bool | Browser confirmation flag | True | | recommendation | str | Remediation guidance | "Sanitize and encode all user input" |

Code Location:

scanner_v2.py:270-279 appends the vulnerability dictionary to self.vulnerabilities list after browser verification completes.

Sources: scanner_v2.py:270-279


Confidence Level Escalation

Escalation Logic

The scanner uses a multi-tier confidence system:

flowchart LR
    Initial["VulnerabilityVerifier.verify_xss()"]
    Low["ConfidenceLevel.LOW"]
    Medium["ConfidenceLevel.MEDIUM"]
    High["ConfidenceLevel.HIGH"]
    Critical["ConfidenceLevel.CRITICAL"]
    
    Initial -->|"Payload reflected, no context"| Low
    Initial -->|"Reflected in executable context"| Medium
    Initial -->|"Unescaped in script context"| High
    
    High -->|"Browser verification fails"| High
    High -->|"Browser verification succeeds"| Critical
    
    Low --> Report["Add to report as LOW"]
    Medium --> Report
    High --> Report
    Critical --> Report["Add to report as CRITICAL"]
    
    style Critical fill:#ffcccc

Escalation Condition:

scanner_v2.py:258-261 escalates confidence from HIGH to CRITICAL when:

  1. Initial pattern detection returns ConfidenceLevel.HIGH
  2. verify_xss_execution() returns is_executed == True

Why Escalation Matters:

  • HIGH confidence indicates likely exploitation based on context analysis
  • CRITICAL confidence provides definitive proof through browser execution
  • Security teams can prioritize CRITICAL findings with confidence they are exploitable

Sources: scanner_v2.py:240-261


Resource Management

Browser Lifecycle Management

The scanner implements proper resource cleanup to prevent memory leaks:

Cleanup Code:

scanner_v2.py:616-621 ensures the browser is stopped after all tests complete:

if self.headless_verifier:
    try:
        await self.headless_verifier.stop()
        Logger.info("Headless browser stopped")
    except Exception as e:
        Logger.error(f"Browser cleanup error: {e}")

Cleanup Timing:

The cleanup occurs at the end of run_heuristic_scan() after all vulnerability tests complete, including:

  • SQL injection tests
  • XSS tests (with browser verification)
  • Command injection tests
  • Path traversal tests
  • XXE tests
  • NoSQL injection tests
  • SSRF tests
  • Session hijacking tests

Exception Handling:

The cleanup is wrapped in try-except to ensure scan completion even if browser shutdown fails. Errors are logged but do not prevent report generation.

Sources: scanner_v2.py:616-621, scanner_v2.py:542-677


Configuration

Enabling Browser Verification

Browser verification can be enabled through multiple interfaces:

Python API:

from wshawk.scanner_v2 import WSHawkV2

scanner = WSHawkV2("ws://target.com")
scanner.use_headless_browser = True  # Enable Playwright verification
await scanner.run_heuristic_scan()

CLI - wshawk-advanced:

The --playwright flag enables browser verification:

wshawk-advanced ws://target.com --playwright

The --full flag enables all features including browser verification:

wshawk-advanced ws://target.com --full

CLI - wshawk (Quick Scan):

The quick scan command has Playwright verification enabled by default (controlled by use_headless_browser = True in scanner initialization).

Sources: scanner_v2.py:53, README.md:99-106, README.md:147-155


Prerequisites

Installing Playwright:

After installing WSHawk, install the Chromium browser for Playwright:

# Install WSHawk
pip install wshawk

# Install Playwright browser
playwright install chromium

Browser Installation Location:

Playwright downloads Chromium to ~/.cache/ms-playwright/ on Linux/macOS or %USERPROFILE%\AppData\Local\ms-playwright\ on Windows. The browser is reused across scans.

Memory Requirements:

  • Minimum: 512MB available RAM
  • Recommended: 1GB+ for concurrent scanning

Sources: README.md:51-58


Performance Considerations

Browser Overhead

Playwright verification introduces performance overhead compared to pattern-matching detection:

| Operation | Time | Memory | Notes | |-----------|------|--------|-------| | Initial browser launch | 2-3 seconds | 100-150MB | One-time cost per scan | | Per-payload verification | 500ms | +10MB per page | Reuses browser instance | | Browser shutdown | <1 second | Freed on exit | Handled in cleanup |

Optimization Strategy:

scanner_v2.py:250-252 implements lazy initialization - the browser is only launched when the first HIGH-confidence XSS candidate is detected. The browser instance is then reused for all subsequent verifications in the same scan.

Cost-Benefit Analysis:

  • Without browser verification: 100+ payloads tested in ~10 seconds, high false positive rate
  • With browser verification: 100+ payloads tested in ~15 seconds, false positive rate reduced by 80%+

The additional 5 seconds provides significant value by confirming exploitability.

Sources: scanner_v2.py:250-256


Scan Timing

Browser verification only occurs for HIGH-confidence XSS candidates, reducing overhead:

flowchart LR
    AllPayloads["100 XSS payloads tested"]
    LowConf["70 payloads: LOW confidence"]
    MedConf["25 payloads: MEDIUM confidence"]
    HighConf["5 payloads: HIGH confidence"]
    BrowserVerify["5 browser verifications"]
    
    AllPayloads --> LowConf
    AllPayloads --> MedConf
    AllPayloads --> HighConf
    
    LowConf -->|"No browser check"| Skip["Skip verification"]
    MedConf -->|"No browser check"| Skip
    HighConf -->|"use_headless_browser=True"| BrowserVerify
    
    BrowserVerify --> Results["5 CRITICAL vulnerabilities"]

Selective Verification:

Only ~5% of XSS payloads typically trigger browser verification, making the overhead acceptable for professional security testing.

Sources: scanner_v2.py:240-261


Report Integration

HTML Report Generation

Browser-verified vulnerabilities appear in the HTML report with additional visual indicators:

Report Fields:

The EnhancedHTMLReporter class (referenced at scanner_v2.py:50, scanner_v2.py:659-663) processes vulnerabilities and generates an HTML report with:

  • CVSS v3.1 scores
  • Screenshots (for browser-verified XSS)
  • Confidence level badges
  • Browser verification status
  • Message replay sequences

Report Generation:

scanner_v2.py:649-670 generates the report after all tests complete, including browser-verified findings.

Report Filename:

Reports are saved as wshawk_report_YYYYMMDD_HHMMSS.html in the current directory.

Sources: scanner_v2.py:649-670, scanner_v2.py:50


Verification Sequence Diagram

End-to-End Verification Flow

sequenceDiagram
    participant CLI as CLI/API
    participant Scanner as WSHawkV2.test_xss_v2()
    participant WS as WebSocket Server
    participant Verifier as VulnerabilityVerifier
    participant Browser as HeadlessBrowserXSSVerifier
    participant Report as EnhancedHTMLReporter
    
    CLI->>Scanner: Start XSS testing
    Scanner->>Scanner: Load WSPayloads.get_xss()
    
    loop For each payload
        Scanner->>WS: Send injected payload
        WS->>Scanner: Response with reflection
        Scanner->>Verifier: verify_xss(response, payload)
        Verifier->>Scanner: (is_vuln=True, confidence=HIGH)
        
        alt confidence == HIGH && use_headless_browser
            Scanner->>Browser: verify_xss_execution(response, payload)
            Browser->>Browser: Launch Chromium (first time only)
            Browser->>Browser: Load response as HTML
            Browser->>Browser: Wait for script execution
            Browser->>Browser: Capture screenshot
            Browser->>Scanner: (is_executed=True, evidence="Console log detected")
            Scanner->>Scanner: Upgrade confidence to CRITICAL
            Scanner->>Scanner: Set browser_verified=True
        end
        
        Scanner->>Scanner: Append to vulnerabilities list
    end
    
    Scanner->>Browser: stop()
    Browser->>Browser: Close Chromium
    
    Scanner->>Report: generate_report(vulnerabilities)
    Report->>Report: Include screenshots + browser_verified flag
    Report->>CLI: wshawk_report_YYYYMMDD_HHMMSS.html

Sources: scanner_v2.py:212-290, scanner_v2.py:616-621, scanner_v2.py:649-670


Verification Workflow Summary

sequenceDiagram
    participant Scanner as WSHawkV2
    participant Verifier as VulnerabilityVerifier
    participant Browser as HeadlessBrowserXSSVerifier
    participant OAST as OASTProvider
    participant Target as Target WebSocket Server
    
    Note over Scanner: XSS Testing
    Scanner->>Target: Send XSS payload
    Target->>Scanner: Response with reflected payload
    Scanner->>Verifier: verify_xss(response, payload)
    Verifier->>Scanner: ConfidenceLevel.HIGH
    
    alt use_headless_browser=True
        Scanner->>Browser: verify_xss_execution(response, payload)
        Browser->>Browser: Load HTML in Chromium
        Browser->>Browser: Wait for script execution
        Browser->>Scanner: (is_executed=True, evidence)
        Scanner->>Scanner: Upgrade to CRITICAL
    end
    
    Note over Scanner: XXE Testing
    alt use_oast=True
        Scanner->>OAST: generate_payload('xxe', 'test0')
        OAST->>Scanner: XXE payload with OAST URL
    end
    
    Scanner->>Target: Send XXE payload
    Target->>Target: Parse XML with external entity
    Target->>OAST: DNS/HTTP callback
    OAST->>OAST: Log callback
    Scanner->>OAST: check_callbacks('test0')
    OAST->>Scanner: Callback received
    Scanner->>Scanner: Confirm XXE vulnerability

Sources: scanner_v2.py:215-293, scanner_v2.py:402-456


Summary

WSHawk's advanced verification mechanisms provide definitive proof of exploitability:

  • Playwright verification confirms XSS payloads execute in real browsers, eliminating false positives from sanitized or encoded reflections
  • OAST integration detects blind vulnerabilities (XXE, SSRF) that produce no visible responses, using out-of-band DNS/HTTP callbacks
  • Both mechanisms integrate seamlessly with the scanner's confidence scoring system, escalating to CRITICAL when verification succeeds
  • Resource management ensures proper cleanup of browser instances and OAST servers after testing

These verification layers distinguish WSHawk from pattern-matching scanners, providing security teams with high-confidence findings backed by concrete evidence.

Sources: scanner_v2.py:28-681, README.md:14-16, CHANGELOG.md:63-65