Perform black-box / grey-box web application penetration testing on an authorized target — auth bypass, IDOR, session handling, business-logic flaws, parameter tampering, Burp Suite / OWASP ZAP workflows. Use when the user mentions 'web pentest,' 'web application penetration test,' 'pentesting,' 'bug bounty,' 'Burp Suite,' 'ZAP,' 'OWASP testing,' 'authentication testing,' 'session testing,' 'authorization testing,' 'web vulnerability testing,' or has explicit authorization to test a live web application.
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Structured black-box / grey-box penetration testing of a live web application against an authorized target. Pairs with recon (which maps the surface) and complements owasp-audit (which reads the source). Use recon first; use this once you have a target list and credentials (or guest access).
Before touching the target, confirm:
If anything is unclear, ask before proceeding. Never assume authorization.
Follows the OWASP Web Security Testing Guide (WSTG) structure. Each phase produces evidence; document everything as you go.
Goal: understand what's running and how it's exposed.
curl -I and curl -I -H "Origin: https://evil.com":
Server, X-Powered-By revealing stackAccess-Control-Allow-Origin reflection without an allow-list, Allow-Credentials: true paired with *testssl.sh https://target or sslyze --regular target/.git/, /.env, /backup.sql, /web.config, /server-status, /.aws/credentials/admin, /administrator, /manage, /console, /actuator/, /_debug/, /swagger, /graphql, /.well-known/Host: internal.target.com?Goal: understand who can be created, how, and with what privileges.
Goal: break in as someone you shouldn't be.
HttpOnly, Secure, SameSite=Lax or Strict?Host: evil.com in the reset email link)state parameter validated? redirect_uri strictly matched (not prefix-matched)?Goal: access things you shouldn't.
Horizontal privilege escalation (IDOR / BOLA):
GET /users/:id, POST /users/:id/..., DELETE /users/:id, GET /api/users/:id/orders, GraphQL user(id: ...)Vertical privilege escalation (BFLA):
/admin/* exploration, from JS bundle strings, from Burp's site map)DELETE /admin/users/x blocked, but POST /admin/users/x/delete works{ role: "admin" } because it does User.create(req.body) without filteringTenant isolation (multi-tenant SaaS):
For each input — URL params, query strings, headers, JSON bodies, file uploads:
<title> injection), stored (in profile fields, comments, file names), DOM (look at JS sinks like innerHTML, eval, location)
owasp-audit Verify Fixes XSS payloads' OR 1=1 --, time-based blind ('; SELECT pg_sleep(5)--), error-based, UNION-based
owasp-audit A01 SSRF bypass matrix{{7*7}} in a name field renders 49? You're inowasp-audit A01 with control-byte rejectionO: for PHP, \xac\xed for Java, cos\nsystem for Python pickle)/health / /status / /metrics / /actuator/* exposedThe category most scanners can't touch. Look at what the app is supposed to do and ask "what if I do that, but wrong?"
-100 and credit yourself?approver_id?.html and have the server serve it with Content-Type: text/html?For depth on invariant modeling, the Actor x Action x Resource matrix, state-machine abuse, limit-slicing, numeric/refund-arbitrage bugs, and the concurrency methodology that proves a double-spend (HTTP/2 single-packet attack, idempotency-key scope, lost-update RMW), use business-logic-testing. Concurrency proofs against money flows need explicit written scope and a request cap — prefer staging / test mode.
view-source: / DevTools, look for inline secrets, API keys, internal endpoint hints<script> and see if it's blockedlocalStorage / sessionStorage / IndexedDB? Tokens? PII?postMessage from your origin reach the app's window without origin check?Run this when you have both the source and a running instance of the same target. owasp-audit reads the code; this skill tests the live app. Together they beat either alone: code tells you where the bug is, live testing proves it is reachable and exploitable. (owasp-audit links here for the source-plus-running-app case.) The Authorization Check above still applies in full to the live target — reading the source does not authorize sending requests.
Build a Target Map. A table linking each code handler to the live endpoint it serves, under one shared auth and config context so a request you craft from the code hits the running instance:
| Code handler | Live endpoint | Auth context | Notes / lead |
|---|---|---|---|
routes/orders.ts cancelOrder() | POST /api/orders/:id/cancel | user cookie | no ownership check before status read |
app/admin/actions.ts promoteUser ('use server') | RPC (no visible route) | admin | server action — enumerate from JS bundle |
Capture shared auth once (the cookie/token for each role and tenant) and the config that maps code to runtime (base URL, route prefixes, framework RPC surfaces like Next.js server actions, tRPC procedures, GraphQL resolvers that do not appear as routes — cross-reference owasp-audit A01).
Two-way prioritization — let each side steer the other:
redirect(req.query.next)) names the exact endpoint, parameter, and precondition to test live. You skip blind fuzzing and go straight to the proof. Order the live tests by the severity the code review assigned.A finding that this mode produces carries both halves of its evidence — the vulnerable code location and the live request/response that proves reachability. Record the verdict (confirmed / needs_validation / rejected) with the paired evidence below.
fuzzing-harness; this skill fuzzes the live app)--risk=1 --level=1 to start)A single request/response does not prove a control failed — it could be the app's normal behavior. Every validated finding carries the attack request AND its baseline/control: two near-identical requests that differ only in the one variable under test, so the difference in response isolates the failing control. Pair by the finding type:
Capture both exchanges in full (request + response) and label which is the control. Note the verdict — confirmed (paired evidence holds), needs_validation (control not yet captured; include the plan), or rejected (the control held) — per references/verification.md.
Write findings.json first, validate it, then render the report from it — per references/findings-format.md. The JSON is the record; prose never changes a verdict, severity, blocker, or demonstrated impact. Run every candidate through the refutation pass in references/verification.md before it is confirmed: the verifier re-reads the captured exchanges and names the strongest control on the path.
Live testing produces evidence files, not source lines. Save each captured exchange — the attack request/response and its paired baseline/control — redacted, to <audit dir>/evidence/<name>.txt, and point location and every evidence[] entry at that file and the line within it. Never save a real session token, password, or customer record: redact a cookie to its name and last 4. In combined white-box + black-box mode, also cite the source handler (path/to/handler.ts:line) in the trace[].
confirmed — the paired evidence holds: two near-identical exchanges differing only in the one variable under test, isolating the failed control (owner vs non-owner, auth vs unauth, payload vs benign; see "Proof-of-concept" above). Paired evidence captured against an authorized target is a dynamic proof, so set reproduced: true and confidence may be high. Example: a non-owner GET /api/orders/42 returns 200 with the order body while the owner's identical request also returns 200 — a BOLA, and the evidence file holds both exchanges.needs_validation — the behaviour is suggestive but the isolating control request isn't captured, or one decisive fact lives in the deployment (a WAF, a proxy that strips a header, an IdP setting). Name the blocker and put the exact command in validation_plan. A check you describe but don't run is needs_validation, not confirmed. Example: a reset email with a Host:-poisoned link, but you haven't confirmed the deployed mailer trusts that header.rejected — a named control at a named location held on every reachable path and failed closed: the non-owner request returned 403/404, the payload was rejected, state was validated. ruled_out_by names the control and where it runs (a middleware, or the deployed header set captured in evidence).Severity goes on confirmed records only, calibrated with references/severity-calibration.md: overall ≤ demonstrated impact. Don't over-rate missing headers, cookie flags, open redirects alone, or clickjacking on non-sensitive actions — those are hardening notes unless you trace a concrete impact. An unsafe shipped default (a permissive CORS default, a hard-coded fallback secret) is confirmed with a system_configuration condition and confidence: medium.
Record domain fields under ext (optional object on any record):
ext key | Meaning | Example |
|---|---|---|
wstg | WSTG test id | WSTG-ATHZ-04 |
endpoint | live endpoint tested | POST /api/orders/{id}/refund |
mode | assessment mode | black-box | grey-box | combined |
Fingerprint: web-pentest/<wstg-or-class>/<endpoint path or resource>/<parameter or operation>, slash-separated, no line numbers (the validator rejects :<digits>, so write a host:port with a slash). Examples: web-pentest/WSTG-ATHZ-04/api/orders/id/refund, web-pentest/reflected-xss/search/q, web-pentest/tls/app.example.com/443/rc4-enabled.
Write findings.json sorted by fingerprint, then validate and fix until it passes:
node references/validate-findings.mjs findings.jsonOptionally export SARIF: node references/to-sarif.mjs findings.json -o findings.sarif. Dispositions (Fixed / Deferred / Accepted Risk / Mitigated (platform/config) / Requires major migration) are set later, usually by finding-triage; a false positive is a rejected record, not a disposition.
Write findings.json first and validate it; the report below is rendered from the JSON and never changes a verdict or severity the JSON doesn't already say.
# Web Application Pentest Report
## Target: [name and scope] ## Methodology: OWASP WSTG ## Mode: [black-box | grey-box | combined]
## findings.json: [path] — validated: [yes] ## Engagement window: [start - end]
### Executive summary
[2-3 paragraphs — overall posture, top findings, business impact]
### Summary
- Confirmed: [N] (Critical [n] | High [n] | Medium [n] | Low [n]) Needs validation: [N] Rejected: [N]
### Confirmed findings
#### [SEVERITY] [Title] `fingerprint`
**Endpoint:** `METHOD /path` **WSTG / CWE:** [WSTG-XXX] / [CWE-XXX] **Confidence:** [medium | high (reproduced)]
**Code location (combined mode):** `path/to/handler.ts:line` — the source behind this endpoint
**What and why it matters:** [the trace, in a few lines]
**PoC — attack request/response:** [from `evidence/<name>.txt`]
**PoC — baseline/control:** [the paired request differing by one variable; label which is the control]
**Fix:** [concrete change] **Verify the fix:** [the check to re-run]
### Needs validation
| Finding | Blockers | Check for the owner to run |
|---|---|---|
### Hardening notes (not findings)
- [gap] — [why it isn't a finding] — [effort]
### Rejected (do not re-raise)
| Fingerprint | Ruled out by | Location |
|---|---|---|
### Out-of-scope / not tested
[Anything skipped, with reason]
### Prioritized remediation plan
1. [highest first — 30 / 60 / 90 day fixes]Severity attaches only to confirmed findings; needs_validation carries blockers[] + a validation_plan and no severity; rejected names the control + location that held. Calibrate severity with references/severity-calibration.md.
references/ at build time): findings-format.md, severity-calibration.md, verification.mdbriiirussell/cybersecurity-skills
c79bb8b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.