Playwright XSS Verification
Playwright XSS Verification
The following files were used as context for generating this wiki page:
- .github/workflows/ghcr-publish.yml
- README.md
- RELEASE_3.0.0.md
- RELEASE_SUMMARY.md
- docs/V3_COMPLETE_GUIDE.md
- requirements.txt
- wshawk/advanced_cli.py
- wshawk/scanner_v2.py
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<script>alert(1)</script> - 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:
confidence == ConfidenceLevel.HIGH- Only verify high-confidence candidatesself.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:
- Create Browser Page: Opens new browser page in existing Chromium instance
- Set HTML Content: Loads the WebSocket response as HTML using
page.set_content() - Monitor Console Events: Attaches listener to capture JavaScript
console.log()calls - Wait for Execution: Waits up to 2 seconds for script execution indicators
- Capture Screenshot: Takes screenshot as visual proof of vulnerability
- Return Evidence: Returns
(is_executed: bool, evidence: str)tuple
Execution Indicators:
The method detects script execution through multiple signals:
console.log(),console.error(), orconsole.warn()eventsalert()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:
- Initial pattern detection returns
ConfidenceLevel.HIGH verify_xss_execution()returnsis_executed == True
Why Escalation Matters:
HIGHconfidence indicates likely exploitation based on context analysisCRITICALconfidence provides definitive proof through browser execution- Security teams can prioritize
CRITICALfindings 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
CRITICALwhen 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