---
description: "Pentester browser DAST playbook — Playwright-based dynamic security probes for app_target surface TS-A08 (browser_exposure). Load when review_contract.target_mode = app_target AND the application has a browser-accessible UI."
---

# Pentester — Browser DAST Playbook

Load this when `review_contract.target_mode = app_target` AND the target application serves a browser-accessible UI (web app, SPA, SSR page).

## Prerequisites

1. Application must be running and accessible at a known URL.
2. Playwright installed: `npm install -g playwright && npx playwright install chromium`
3. Verify readiness: `aioson qa:doctor`

## Phase 0 — Baseline DAST (mandatory pre-step)

Before running any manual Playwright probe, execute the AIOSON automated DAST baseline:

```bash
# Full run with hacker persona (secrets, XSS, IDOR, open redirect, SQL injection, debug routes)
aioson qa:run --persona=hacker --url=<target-url>

# Autonomous crawl scan (discovers routes, probes each one)
aioson qa:scan --url=<target-url> --depth=3 --max-pages=50
```

Read `aios-qa-report.json` and import any `critical` or `high` findings into the pentester findings artifact as `status: needs_validation`. The pentester validates or reclassifies each — `qa:run` automates detection, pentester applies adversarial judgment.

**Do NOT skip Phase 0.** The automated baseline catches low-hanging fruit (exposed secrets, accessible .env files, basic XSS) without burning manual probe time. Phase 1+ targets what automation misses.

## Phase 1 — Security Headers Audit

Navigate to the target URL with Playwright and capture response headers on the main document request.

### Mandatory headers to check

| Header | Expected | Severity if missing/weak |
|---|---|---|
| `Content-Security-Policy` | Present, no `unsafe-inline` for scripts, no `unsafe-eval` | `high` if missing entirely, `medium` if has `unsafe-inline`/`unsafe-eval` |
| `Strict-Transport-Security` | `max-age>=31536000; includeSubDomains` | `medium` (localhost exempt) |
| `X-Content-Type-Options` | `nosniff` | `medium` |
| `X-Frame-Options` | `DENY` or `SAMEORIGIN` (check CSP `frame-ancestors` too) | `medium` |
| `Referrer-Policy` | `strict-origin-when-cross-origin` or stricter | `low` |
| `Permissions-Policy` | Present, restricts `camera`, `microphone`, `geolocation` at minimum | `low` |
| `X-XSS-Protection` | `0` (modern recommendation — CSP supersedes; `1; mode=block` causes info leaks in old IE) | `info` |

### Headers that SHOULD NOT be present

| Header | Why | Severity |
|---|---|---|
| `Server` with version (e.g. `nginx/1.24.0`) | Reveals infrastructure version — aids targeted exploits | `low` |
| `X-Powered-By` | Reveals framework (Express, PHP, ASP.NET) | `low` |
| `X-AspNet-Version` / `X-AspNetMvc-Version` | .NET version disclosure | `low` |

### Playwright probe pattern

```javascript
const response = await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
const headers = response.headers();

// Check presence and value of each security header
const csp = headers['content-security-policy'] || '';
const hsts = headers['strict-transport-security'] || '';
const xcto = headers['x-content-type-options'] || '';
const xfo = headers['x-frame-options'] || '';
const rp = headers['referrer-policy'] || '';
const pp = headers['permissions-policy'] || '';

// Disclosure headers (should NOT be present with version info)
const server = headers['server'] || '';
const poweredBy = headers['x-powered-by'] || '';
```

**ASVS:** V3.4 (CSP), V12.1 (HSTS), V13.1.5 (security headers), V14.3.3 (server disclosure).

## Phase 2 — Cookie Security Audit

After authentication (if credentials are available), inspect all cookies set by the application.

### Required attributes for session/auth cookies

| Attribute | Expected | Severity if missing |
|---|---|---|
| `Secure` | `true` (except localhost) | `high` |
| `HttpOnly` | `true` for session tokens | `high` |
| `SameSite` | `Lax` or `Strict` | `medium` |
| `Path` | Narrowest scope needed (prefer `/` only if necessary) | `info` |
| `__Host-` prefix | Recommended for session cookies — enforces Secure + Path=/ + no Domain | `info` |
| `__Secure-` prefix | Alternative — enforces Secure flag | `info` |
| Max-Age / Expires | Session cookies should not persist beyond browser close unless explicitly justified | `low` |

### Playwright probe pattern

```javascript
const cookies = await context.cookies();
for (const cookie of cookies) {
  const isSession = /session|token|auth|sid|jwt/i.test(cookie.name);
  if (isSession) {
    // Check Secure flag
    if (!cookie.secure) { /* HIGH finding */ }
    // Check HttpOnly flag
    if (!cookie.httpOnly) { /* HIGH finding */ }
    // Check SameSite
    if (cookie.sameSite === 'None' && !cookie.secure) { /* MEDIUM finding */ }
    if (!cookie.sameSite || cookie.sameSite === 'None') { /* MEDIUM finding — CSRF surface */ }
  }
}
```

**ASVS:** V7.1.1 (cookie attributes), V7.1.2 (HttpOnly), V7.1.3 (SameSite).

## Phase 3 — Client-Side Storage Audit

Check localStorage and sessionStorage for sensitive data that should never be stored client-side.

### Sensitive patterns to flag

| Pattern | Risk | Severity |
|---|---|---|
| JWT tokens (eyJ...) | Token theft via XSS gives full account takeover | `high` |
| API keys (sk-*, pk_*, AKIA*, AIzaSy*) | Direct API abuse | `critical` |
| Passwords / password hashes | Direct compromise | `critical` |
| PII (email + name + phone + address combined) | Privacy violation, GDPR exposure | `medium` |
| Credit card numbers / CVV | PCI-DSS violation | `critical` |
| Session IDs | Session hijacking via XSS | `high` |

### Playwright probe pattern

```javascript
const storageData = await page.evaluate(() => {
  const data = { localStorage: {}, sessionStorage: {} };
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    data.localStorage[key] = localStorage.getItem(key);
  }
  for (let i = 0; i < sessionStorage.length; i++) {
    const key = sessionStorage.key(i);
    data.sessionStorage[key] = sessionStorage.getItem(key);
  }
  return data;
});

// Check each value against SECRET_PATTERNS and JWT regex
const jwtRegex = /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/;
```

**ASVS:** V14.1.3 (sensitive data in client storage), V8.2.2 (no sensitive data in browser storage).

## Phase 4 — CORS Misconfiguration

Test the server's CORS policy by sending requests with crafted `Origin` headers.

### Probes

1. **Wildcard origin**: send `Origin: https://evil.com` — if response has `Access-Control-Allow-Origin: *`, flag it.
2. **Reflected origin**: send `Origin: https://evil.com` — if response reflects `Access-Control-Allow-Origin: https://evil.com`, flag it.
3. **Null origin**: send `Origin: null` — some misconfigured servers allow this.
4. **Credentials with wildcard**: `Access-Control-Allow-Credentials: true` with `Access-Control-Allow-Origin: *` is a browser-rejected but server-misconfigured pattern.
5. **Subdomain regex bypass**: `Origin: https://evil-example.com` or `Origin: https://example.com.evil.com` — catches poor regex matching.

### Playwright probe pattern

```javascript
// Playwright doesn't set custom Origin on navigation, use page.evaluate with fetch
const corsResult = await page.evaluate(async (targetUrl) => {
  try {
    const res = await fetch(targetUrl, { mode: 'cors', credentials: 'include' });
    return {
      acao: res.headers.get('access-control-allow-origin'),
      acac: res.headers.get('access-control-allow-credentials')
    };
  } catch { return { blocked: true }; }
}, apiEndpoint);
```

For deeper CORS testing, use `page.route()` to intercept and modify request headers, or use the Playwright `request` API directly.

**ASVS:** V4.3.1, V4.3.2 (CORS policy).

## Phase 5 — Source Map Exposure

Source maps (`.js.map`) in production reveal the full original source code, including comments, variable names, and internal logic.

### Probes

1. Capture all JS file URLs loaded by the page.
2. For each `*.js` URL, try fetching `*.js.map`.
3. Check the HTML source for `//# sourceMappingURL=` directives.
4. Check response headers for `SourceMap:` or `X-SourceMap:` headers.

### Playwright probe pattern

```javascript
const jsFiles = [];
page.on('response', (response) => {
  if (response.url().endsWith('.js') && response.status() === 200) {
    jsFiles.push(response.url());
  }
});

await page.goto(targetUrl, { waitUntil: 'networkidle' });

for (const jsUrl of jsFiles) {
  const mapUrl = jsUrl + '.map';
  try {
    const mapResponse = await page.goto(mapUrl, { waitUntil: 'commit', timeout: 3000 });
    if (mapResponse && mapResponse.status() === 200) {
      const body = await mapResponse.text();
      if (body.includes('"sources"') || body.includes('"mappings"')) {
        // HIGH finding — source map accessible
      }
    }
  } catch { /* not accessible — good */ }
}
```

**Severity:** `high` (reveals full source code including business logic, API routes, internal comments).

**Fix:** Remove source maps from production builds. Configure bundler (webpack/vite/next) to exclude `.map` files from production output, or restrict via web server rules.

## Phase 6 — Clickjacking (Frame Injection)

Test if the target page can be embedded in an iframe on an attacker-controlled domain.

### Probes

1. Check `X-Frame-Options` header (covered in Phase 1).
2. Check CSP `frame-ancestors` directive (more granular than X-Frame-Options).
3. If neither is set, attempt to iframe the page:

```javascript
// Create a test page that iframes the target
await page.setContent(`
  <iframe id="target" src="${targetUrl}" width="800" height="600"></iframe>
`);
await page.waitForTimeout(2000);

const iframeLoaded = await page.evaluate(() => {
  const iframe = document.getElementById('target');
  try {
    // Cross-origin iframe access will throw — but loading without error means framing is allowed
    return iframe.contentWindow.location.href !== 'about:blank';
  } catch { return true; } // cross-origin = loaded but blocked by same-origin policy (still frameable)
});
```

**ASVS:** V3.7 (clickjacking), V13.1.5 (frame protection).

## Phase 7 — Subresource Integrity (SRI)

Scripts and stylesheets loaded from CDNs or third-party origins should have `integrity` attributes to prevent supply-chain attacks (CDN compromise, DNS hijacking).

### Probes

```javascript
const sriIssues = await page.evaluate(() => {
  const issues = [];
  const scripts = document.querySelectorAll('script[src]');
  for (const s of scripts) {
    const src = s.getAttribute('src') || '';
    const isExternal = src.startsWith('http') && !src.includes(window.location.hostname);
    if (isExternal && !s.getAttribute('integrity')) {
      issues.push({ type: 'script', src: src.substring(0, 120) });
    }
  }
  const links = document.querySelectorAll('link[rel="stylesheet"][href]');
  for (const l of links) {
    const href = l.getAttribute('href') || '';
    const isExternal = href.startsWith('http') && !href.includes(window.location.hostname);
    if (isExternal && !l.getAttribute('integrity')) {
      issues.push({ type: 'stylesheet', src: href.substring(0, 120) });
    }
  }
  return issues;
});
```

**Severity:** `medium` per external resource without SRI. `high` if the resource is a payment or auth SDK.

**ASVS:** V15.2 (SRI).

## Phase 8 — Error Page Information Disclosure

Trigger error conditions and inspect the response for framework-default error pages that leak internal information.

### Probes

1. **404 page**: navigate to a random non-existent path. Check for stack traces, framework names, file paths.
2. **500 trigger**: if any form/endpoint is accessible, submit malformed data designed to cause server errors.
3. **Verbose errors**: check if the response contains path separators (`/`, `\`), line numbers (`at line`), or framework markers (`Django`, `Rails`, `Express`, `Next.js`, `Laravel`, `Spring`).

```javascript
const errorPage = await page.goto(`${targetUrl}/nonexistent-${Date.now()}`, {
  waitUntil: 'domcontentloaded', timeout: 5000
});
const errorHtml = await page.content();

const leakPatterns = [
  /at\s+\w+\s+\(.*:\d+:\d+\)/,        // JS stack trace
  /File ".*", line \d+/,                // Python traceback
  /vendor\/.*\.php:\d+/,                // PHP stack trace
  /SQLSTATE\[/,                          // SQL error
  /Traceback \(most recent/,            // Python traceback header
  /(Django|Laravel|Rails|Express|Spring|Next\.js) (Debug|Error)/i
];
```

**Severity:** `medium` for framework name disclosure, `high` for full stack traces with file paths.

**ASVS:** V13.1.3 (error handling), V16.2 (information disclosure).

## Phase 9 — HTML Meta & Comment Leaks

Inspect the HTML source for metadata and comments that disclose internal information.

### Probes

```javascript
const metaLeaks = await page.evaluate(() => {
  const issues = [];

  // Generator meta tags
  const generator = document.querySelector('meta[name="generator"]');
  if (generator) issues.push({ type: 'generator', value: generator.content });

  // HTML comments with sensitive content
  const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
  while (walker.nextNode()) {
    const text = walker.currentNode.textContent.trim();
    if (/(TODO|FIXME|HACK|password|secret|key|token|debug)/i.test(text)) {
      issues.push({ type: 'comment', value: text.substring(0, 100) });
    }
  }

  // Hidden inputs with sensitive-looking names
  const hiddenInputs = document.querySelectorAll('input[type="hidden"]');
  for (const input of hiddenInputs) {
    const name = input.name || '';
    if (/(token|secret|key|csrf|nonce)/i.test(name) && input.value.length > 20) {
      issues.push({ type: 'hidden_input', name, valueLength: input.value.length });
    }
  }

  return issues;
});
```

**Severity:** `low` for generator tags, `medium` for TODO/debug comments, `info` for CSRF tokens in hidden inputs (expected pattern, but verify they rotate).

## Integration with pentester findings artifact

Map every finding from this playbook to the standard pentester finding schema:

```json
{
  "id": "SF-{slug}-NN",
  "feature_slug": "{slug}",
  "surface": "app_target_browser_exposure",
  "severity": "...",
  "title": "...",
  "hypothesis": "Browser-based DAST probe via Playwright",
  "preconditions": ["Application running at {url}", "Playwright + Chromium installed"],
  "reproduction_steps": ["1. Run aioson qa:run --persona=hacker", "2. ..."],
  "evidence": ["Response header dump", "Screenshot path"],
  "impact": "...",
  "affected_artifacts": ["URL or source file path"],
  "suggested_fix": "...",
  "recommended_owner": "dev",
  "recommended_gate_status": "...",
  "status": "open",
  "safe_to_reproduce": true,
  "asvs_ids": ["V..."]
}
```

## Summary — probe priority order

| Phase | What | Time estimate | Severity ceiling |
|---|---|---|---|
| 0 | `qa:run --persona=hacker` + `qa:scan` baseline | 2-5 min | critical |
| 1 | Security headers | 30s | high |
| 2 | Cookie attributes | 30s | high |
| 3 | Client-side storage | 30s | critical |
| 4 | CORS misconfiguration | 1 min | high |
| 5 | Source map exposure | 1 min | high |
| 6 | Clickjacking | 30s | medium |
| 7 | SRI (CDN resources) | 30s | medium |
| 8 | Error page disclosure | 30s | high |
| 9 | HTML meta & comment leaks | 30s | medium |

Total additional time beyond Phase 0: ~5-6 minutes of automated probing.

## References

- OWASP ASVS 5.0: Chapters V3, V4, V7, V8, V12, V13, V14, V15, V16
- OWASP Testing Guide v4.2: OTG-CONFIG, OTG-INFO, OTG-SESS
- Mozilla Observatory scoring methodology
- Playwright API: https://playwright.dev/docs/api/class-response
