---
description: "Pentester deep playbooks for app_target surfaces TS-A01..A07 — IDOR/BOLA, secrets/crypto, injection/XSS, race/insecure design, auth/rate-limit. Load when review_contract.target_mode = app_target."
---

# Pentester — App Target Playbooks

Load this when `review_contract.target_mode = app_target`. Each section is a step-by-step playbook for one of the mandatory app surfaces. Map every finding with `severity ≥ medium` to one or more **OWASP ASVS 5.0** requirement IDs (e.g. `ASVS V8.1.1`).

## ASVS 5.0 — verification levels

| Level | Use when |
|---|---|
| L1 | Quick adoption baseline; internal tools |
| L2 | Default for consumer apps |
| L3 | High assurance — finance, health, government, irreversible actions |

The 17 chapters: V1 Encoding/Sanitization · V2 Validation/Business Logic · V3 Web Frontend · V4 API/Web Service · V5 File Handling · V6 Authentication · V7 Session Management · V8 Authorization · V9 Self-Contained Tokens · V10 OAuth/OIDC · V11 Cryptography · V12 Secure Communication · V13 Configuration · V14 Data Protection · V15 Secure Coding · V16 Security Logging · V17 WebRTC.

## TS-A01 — `app_target_ownership_idor` / BOLA

**Why DAST misses this:** request-level fuzzing has no concept of ownership. The endpoint returns 200 either way — the attacker is just reading someone else's resource.

**Setup (mandatory before testing):**
1. Provision **two distinct user accounts** (`alice`, `bob`), authenticate both, capture both tokens.
2. Seed each with their own resources (alice's order, alice's file, alice's profile; same for bob).
3. Record the IDs alice owns: orders, files, profile fields, comments, payment methods, anything keyed by user_id.

**Test loop — for every endpoint that takes an object identifier:**
```
For each method in {GET, PATCH, PUT, DELETE, POST}:
  For each ID alice owns:
    Replay the request as bob (bob's token, alice's IDs).
    Expected: 403 (preferred) or 404 (acceptable).
    Fail signals:
      - 200 with alice's data → horizontal IDOR confirmed
      - 200 modifying alice's data → write-IDOR (high severity)
      - 403 vs 404 timing leak → existence oracle (medium)
      - 500 / stack trace → info disclosure
```

**Common false-secure pattern:** filter by user_id in the SELECT but not in UPDATE/DELETE. The GET returns empty (looks safe) but PATCH succeeds.

**Vertical IDOR (privilege escalation):** alice is regular user. Try every admin-only endpoint with alice's token. Expected: 403. Fail: 200.

**Where to look:**
- URL path params: `/orders/:id`, `/users/:id`, `/files/:id`
- Query strings: `?orderId=`, `?fileId=`
- Body fields: `{ "userId": "...", "ownerId": "..." }`
- Headers: `X-Tenant-Id`, `X-User-Id`
- Cookies that encode user identity beyond the session token

**ASVS:** V8.1.1, V8.2.1, V8.3.1.

**Suggested fix (for the `@dev` handoff):** middleware that fetches the resource AND checks `resource.owner_id === jwt.sub` before any handler logic. Centralized — never per-route.

## TS-A02 — `app_target_secrets_crypto`

**Probes:**
1. **Repo grep** for hardcoded secrets — but trust SAST tools more (Gitleaks for fast pre-commit pattern match, TruffleHog for credential verification — TruffleHog actually validates that detected strings are live secrets, not just regex matches).
2. **`.env` and config inspection**: any `_KEY`, `_SECRET`, `_TOKEN`, `_PASSWORD`, `_DSN` not loaded from env or vault.
3. **Logs and error messages**: secrets leaking in stack traces, request logs, devlogs.
4. **API responses**: sensitive fields in JSON (API keys, password hashes, TOTP secrets, internal IDs).
5. **JWT inspection**: weak secret (try `jwt-secret-list`), `alg:none` accepted, signature not verified, algorithm confusion (RS256 → HS256 with public key as secret).
6. **Crypto choice**: passwords stored with Argon2id (preferred), bcrypt (acceptable cost ≥ 10), scrypt, PBKDF2 (≥ 600k iter SHA-256). Reject MD5, SHA-1, plain SHA-256, plain SHA-2.
7. **Symmetric crypto**: AES-GCM with random nonce; reject ECB, reject static IV.

**ASVS:** V11.1.1 (algorithm choice), V11.6.1 (password hashing), V14.1.1 (secret storage).

**Tools:** Gitleaks (regex-fast), TruffleHog (verified), Semgrep `p/secrets`.

## TS-A03 — `app_target_injection_xss`

**SQL injection:**
- Probe: classic `' OR 1=1 --` and time-based `'; SELECT pg_sleep(5); --` only against fixtures or dedicated test instances. Never against production data.
- Tools: Semgrep `p/sql-injection`, ZAP active scanner, sqlmap (controlled environment only).
- Fix sentinel: parameterized queries / ORM with placeholders. **No `f"... {user_input} ..."` SQL strings.**

**XSS (reflected, stored, DOM):**
- Probe: payloads `<script>1</script>`, `<img src=x onerror=alert(1)>`, `"><svg/onload=alert(1)>`, attempted in every input that ends up rendered.
- Output context matters: HTML body, attribute, JS context, URL, CSS — each needs different escaping.
- React/Vue: `dangerouslySetInnerHTML` / `v-html` with user data → XSS sink. Sanitize with DOMPurify if unavoidable.
- CSP header: verify `default-src 'self'`, no `'unsafe-inline'` for scripts.

**Template injection (Jinja2, Twig, Handlebars):**
- Probe: `{{7*7}}` rendering as `49` confirms server-side template injection. RCE typically follows.
- Sentinel: user input never reaches template *source* — only template *data*.

**Prototype pollution (JS/TS):**
- Probe: `Object.assign(target, JSON.parse('{"__proto__": {"isAdmin": true}}'))`.
- Sentinel: `lodash.merge` / `Object.assign` patterns; check `Object.create(null)` for trusted assignments.

**ASVS:** V1.2 (encoding), V15.1 (secure coding).

## TS-A04 — `app_target_insecure_design_race` (TOCTOU)

**Why DAST misses this:** race conditions need parallel requests with sub-millisecond arrival. Sequential scanners can't trigger them.

**Common attack patterns to test:**
1. Double-spend: gift card / coupon redeemed twice, balance going negative.
2. Concurrent registration creating duplicate usernames.
3. Concurrent state transitions (publish + delete, accept + cancel an order).
4. Simultaneous file upload bypassing quota.

**Method — last-byte synchronization:**
```
1. Open N parallel HTTP/1.1 sockets to the target endpoint.
2. Send all bytes of each request EXCEPT the final byte.
3. Send the final byte on all sockets simultaneously.
4. Inspect responses — if more than one returns success on a single-use action, race confirmed.
```

**HTTP/2 alternative:** single connection, multiplex N requests, all arrive in the same time window.

**Tools:**
- **Burp Suite Turbo Intruder** — race-condition mode does last-byte sync automatically.
- **Burp's built-in repeater "Send group in parallel (single packet)"** — newer feature, works on HTTP/2.
- Custom Node script using `http2.connect` + `Promise.all(streams.map(s => s.end(...)))`.

**Multi-agent / agentic apps:** wider timing windows than conventional code (slow LLM calls, async tools). Race surface is *larger*, not smaller.

**ASVS:** V2.1, V2.4 (business logic + race conditions).

**Fix patterns (handoff to `@dev`):** SELECT FOR UPDATE on the row before mutation; idempotency keys with database UNIQUE constraints; optimistic locking (version column with retry on CAS miss).

## TS-A07 — `app_target_auth_rate_limit`

**Mandatory tests, mapped to ASVS V6/V7:**

1. **Brute-force protection (V6.2):** rate limit per IP and per account. Test: send 11 wrong passwords from same IP for one account. Expected: lockout / captcha by attempt 6–10. Fail: unlimited attempts.
2. **Credential stuffing distinguished from brute-force:** test high-volume from rotating IPs against many accounts (one attempt per IP). Expected: account-side limiter still triggers. Tools: ATO simulators.
3. **Account enumeration (V6.3):** measure response time and content for "user-not-found" vs "wrong-password". Identical → safe. Different → enumeration leak.
4. **Bypass via auth-adjacent endpoints:** rate limiter often only on `/login`. Try `/api/auth/refresh`, `/forgot-password`, `/api/users/exists`, `/auth/social-callback`. All auth paths must share the limiter.
5. **MFA bypass:**
   - Replay: capture an OTP, use it twice. Expected: rejected as already-used.
   - Skip step: directly call the endpoint that requires MFA without completing step 2.
   - Race condition on OTP validation (TOCTOU on attempt counter).
6. **Session management (V7.1, V7.5):** logout invalidates token server-side; password change rotates session; cookies have `Secure` + `HttpOnly` + `SameSite=Lax/Strict`.
7. **Password reset:** token entropy (≥ 128 bits), single-use, time-bound (≤ 1 hour), bound to account, not predictable.
8. **OAuth/OIDC (V10):** `redirect_uri` whitelist not bypassable (no path traversal, no `evil.com.legit.com`); `state` parameter required and verified; PKCE for public clients.

**ASVS:** V6.1–V6.4, V7.1, V7.5, V10.1–V10.3.

## TS-A05 — `app_target_logging_monitoring` (often skipped)

If the feature has security-relevant events (login, privilege change, money transfer, deletion), verify:
- Each event produces a log entry with: who, what, when, source IP, outcome.
- Logs do NOT contain secrets (passwords, tokens, full credit cards).
- Logs are tamper-resistant (append-only, signed, or shipped off-host).

**ASVS:** V16.1, V16.2.

## TS-A06 — `app_target_ssrf` (when feature fetches URLs)

When the app accepts a URL from the user and fetches it (avatar import, webhook, OAuth issuer discovery, link unfurl):
1. Cloud metadata: `http://169.254.169.254/latest/meta-data/` (AWS), `http://metadata.google.internal/` (GCP).
2. Internal IPs: `127.0.0.1`, `10.0.0.0/8`, `192.168.0.0/16`, `172.16.0.0/12`.
3. Localhost ports: 22, 3306, 5432, 6379, 9200.
4. DNS rebinding: domain that resolves to public IP at first lookup, internal IP at second.
5. Redirects: server fetches `https://attacker.com/redirect → http://internal-host`.

**Fix:** allow-list of permitted hostnames/CIDRs; deny private ranges + cloud metadata IPs explicitly; resolve DNS once and pin the result for the request.

**ASVS:** V12.5, V13.4.

## Reporting — ASVS-anchored finding schema

Add to every app_target finding with `severity ≥ medium`:

```json
{
  "id": "SF-{slug}-NN",
  "surface": "app_target_ownership_idor",
  "asvs_ids": ["V8.1.1", "V8.2.1"],
  "severity": "high",
  "title": "...",
  "attack_path": "alice's order id substituted in PATCH as bob",
  "preconditions": ["two seeded accounts", "alice owns order #42"],
  "reproduction_steps": ["1. Login as bob", "2. PATCH /orders/42 with bob's token", "..."],
  "evidence": ["request log: 200 OK", "response body shows alice's data modified"],
  "impact": "horizontal write-IDOR on orders — any user can mutate any order",
  "affected_artifacts": ["src/api/orders.ts:88"],
  "suggested_fix": "centralize ownership check in middleware (see V8.2.1 reference impl)",
  "recommended_owner": "dev",
  "recommended_gate_status": "block"
}
```

## Tool stack to actually run

Minimum baseline for an `app_target` review (cite versions in the review_contract):

| Concern | Tool | Why |
|---|---|---|
| SAST | **Semgrep CE** + curated rules `p/security-audit`, `p/owasp-top-ten` | 30+ langs, low FP with ruleset |
| SAST (GitHub) | **CodeQL** | Free public repos via GHAS, semantic queries |
| SCA + container | **Trivy** | Multi-purpose, supports SBOM output |
| DAST | **OWASP ZAP** baseline scan | Free, mature, CI-friendly |
| Secrets | **Gitleaks** (pre-commit) + **TruffleHog** (verified) | Different roles — both useful |
| IaC | **Checkov** | Terraform / K8s / Helm |
| LLM-app | **Garak** | Adversarial prompt fuzzing |

For race conditions and IDOR, **no scanner replaces manual playbooks** in the sections above.

## References

This document distills `researchs/pentester-app-playbooks-2026/summary.md`. See that file for the full source list and verdict.
