About web-pentest
web-pentest is a skill for authorized black-box or grey-box testing of live web applications. Its OWASP WSTG-based phases cover application controls, with paired baseline and attack evidence required for findings.
Key features
- 01OWASP WSTG-structured testing
- 02Authentication and access-control checks
- 03Input-validation and business-logic tests
- 04Source and live-instance correlation
- 05Paired evidence for findings
Use cases
- 01Assess an authorized live web application
- 02Test for IDOR, privilege, and session flaws
- 03Turn source-review leads into live tests
- 04Collect reproducible evidence for findings
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ briiirussell/ cybersecurity-skills --skill web-pentestAfter installing, type/web-pentestin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/findings-format.mdfindings.schema.jsonseverity-calibration.mdto-sarif.mjsvalidate-findings.mjsverification.mdDetails
From the project READMEWeb Pentest — Live Web Application Testing
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).
Authorization Check
Before touching the target, confirm:
- Written authorization for this specific application (pentest engagement, bug bounty in-scope domain, CTF/lab, your own asset)
- The application is currently in scope and live (not deprecated, not under maintenance freeze)
- Test credentials provided (if grey-box), or guest access confirmed (if black-box)
- Out-of-scope items documented — production user data, payment flows, social engineering, DoS
If anything is unclear, ask before proceeding. Never assume authorization.
Methodology
Follows the OWASP Web Security Testing Guide (WSTG) structure. Each phase produces evidence; document everything as you go.
Phase 1: Configuration & deployment
Goal: understand what's running and how it's exposed.
- HTTP headers — pull with
curl -Iandcurl -I -H "Origin: https://evil.com":Server,X-Powered-Byrevealing stack- HSTS, CSP, X-Frame-Options, X-Content-Type-Options presence and values
- CORS —
Access-Control-Allow-Originreflection without an allow-list,Allow-Credentials: truepaired with*
- TLS —
testssl.sh https://targetorsslyze --regular target - Backups / configs exposed —
/.git/,/.env,/backup.sql,/web.config,/server-status,/.aws/credentials - Default admin paths —
/admin,/administrator,/manage,/console,/actuator/,/_debug/,/swagger,/graphql,/.well-known/ - Robots.txt and sitemap.xml — often list non-indexed but accessible endpoints
- Subdomain & host header testing — does the app behave differently when sent
Host: internal.target.com?
Phase 2: Identity management
Goal: understand who can be created, how, and with what privileges.
- Registration — can you self-register? Does the role default to anything other than "user"?
- Username enumeration via differential responses on signup ("email already exists"), login ("user not found" vs "wrong password"), password reset ("we sent an email" vs "no such user")
- Account provisioning — does a verification email arrive? Can it be skipped? Does the reset link expire? Does the reset link tie to the original session?
- Account lockout vs rate-limit — does 100 failed logins lock the account (denial of service vector) or just slow down (good)?
Phase 3: Authentication
Goal: break in as someone you shouldn't be.
- Login form — is HTTPS enforced? Are credentials sent in URL params (logged everywhere)?
- Password policy — minimum length / complexity / breach-database check (haveibeenpwned API)
- MFA — required for admin / privileged users? Bypass paths (recovery code, "remember this device" cookie persistence, downgrade to SMS)?
- Session token — entropy looks high? Cookie has
HttpOnly,Secure,SameSite=LaxorStrict? - Session fixation — does the session ID rotate on login? Pre-auth session ID still valid post-auth?
- "Remember me" — long-lived token in a separate cookie; revocable on logout from all devices?
- Logout — does it actually invalidate the session server-side? (Test by replaying the cookie post-logout.)
- Password reset — token format, expiration, single-use, host-header poisoning (
Host: evil.comin the reset email link) - OAuth flow — PKCE used?
stateparameter validated?redirect_uristrictly matched (not prefix-matched)?
Phase 4: Authorization (the highest-yield phase)
Goal: access things you shouldn't.
Horizontal privilege escalation (IDOR / BOLA):
- Sign in as user A. Get a resource that belongs to user A. Note the ID.
- Sign in as user B. Try to access user A's resource by ID — direct fetch, in the body of an update, as a foreign key in another operation.
- Watch for response codes — 200 (bad), 403 (good), 404 (good if all unauthorized requests return 404; bad if your own 404 looks different)
- Try every endpoint that takes an ID —
GET /users/:id,POST /users/:id/...,DELETE /users/:id,GET /api/users/:id/orders, GraphQLuser(id: ...)
Vertical privilege escalation (BFLA):
- Identify admin-only routes (from
/admin/*exploration, from JS bundle strings, from Burp's site map) - Hit them as a regular user. 403 (good), 200 (very bad)
- Check for verb tampering —
DELETE /admin/users/xblocked, butPOST /admin/users/x/deleteworks - Mass assignment — registration endpoint that accepts
{ role: "admin" }because it doesUser.create(req.body)without filtering
Tenant isolation (multi-tenant SaaS):
- Create org A with user U1. Create org B with user U2.
- As U1, try every URL that includes the org/tenant ID and swap it to org B's. Expect 403 / 404 on every one. A 200 is a tenancy break.
Phase 5: Session management
- Session token in URL? Anywhere logged → access logs leak it
- Concurrent sessions allowed? Should be configurable / loggable
- Token after password change — invalidated everywhere? (Test by changing password in browser 1 and replaying browser 2's cookie.)
- CSRF protection — synchronizer token, SameSite cookie, custom request header? Test by forging a request from a different origin
Phase 6: Input validation
For each input — URL params, query strings, headers, JSON bodies, file uploads:
- XSS — reflected (in error pages, search results,
<title>injection), stored (in profile fields, comments, file names), DOM (look at JS sinks likeinnerHTML,eval,location)- Payload bank — see
owasp-auditVerify Fixes XSS payloads
- Payload bank — see
- SQLi —
' OR 1=1 --, time-based blind ('; SELECT pg_sleep(5)--), error-based, UNION-based- Modern stacks usually use parameterized queries; look for the one endpoint that didn't get the memo
- Command injection — anywhere file names, image processing, PDF generation, or shell-out happens
- SSRF — webhook URLs, image-fetch, PDF render — see
owasp-auditA01 SSRF bypass matrix - XXE — XML inputs in SOAP / SVG-upload / DOCX-import paths
- Server-side template injection —
{{7*7}}in a name field renders49? You're in - Open redirect — see
owasp-auditA01 with control-byte rejection - Insecure deserialization — base64-decode any opaque tokens and look for serialized format markers (
O:for PHP,\xac\xedfor Java,cos\nsystemfor Python pickle)
Phase 7: Error handling
- Stack traces visible in 500 responses
- Database errors leak schema (table names, column names)
- File paths leak server filesystem layout
- Different errors for valid vs invalid usernames (enumeration)
/health//status//metrics//actuator/*exposed
Phase 8: Business logic
The category most scanners can't touch. Look at what the app is supposed to do and ask "what if I do that, but wrong?"
- Race conditions — submit the same coupon code 100× concurrently; can you redeem it more than once?
- Negative numbers — can you transfer
-100and credit yourself? - Out-of-order workflows — can you complete step 4 before step 2?
- Quota bypass — free tier limits enforced server-side or just in the UI?
- Workflow approval — can you approve your own request by tampering with
approver_id? - File upload — can you upload an
.htmland have the server serve it withContent-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.
Phase 9: Client-side
- JS bundle review —
view-source:/ DevTools, look for inline secrets, API keys, internal endpoint hints - CSP — try to inject a
<script>and see if it's blocked - Browser storage — what's in
localStorage/sessionStorage/ IndexedDB? Tokens? PII? - Cross-origin — can
postMessagefrom your origin reach the app's window without origin check?
Combined white-box + black-box mode
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:
- Code -> live. A source finding (a missing ownership predicate, a check-then-act window, a
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. - Live -> code. A live anomaly (a 200 where you expected 403, a differential timing, an error that leaks a column name) sets the priority for the next code read — open that handler and trace why the guard did not fire. Treat the pairing as a loop, not a handoff: each confirmed item on one side re-prioritizes the other.
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.
Tooling
- Burp Suite Community (free) — proxy, repeater, intruder (rate-limited), decoder
- Burp Suite Pro — scanner, faster intruder, session handling rules
- OWASP ZAP — free alternative, active scanner, fuzzer
- ffuf / feroxbuster / gobuster — directory and parameter brute-forcing
- sqlmap — SQLi exploitation (use carefully; can be noisy and destructive —
--risk=1 --level=1to start) - dalfox / XSStrike — XSS scanners (high false-positive; use as a starting point)
- nuclei — template-driven vulnerability scanner; the templates themselves are excellent reading
- curl / httpie — repeatable manual requests; everything you find should reduce to a curl command
Proof-of-concept — paired evidence required
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:
- Access control (IDOR/BOLA, BFLA, tenancy): owner vs. non-owner. Two identical requests — same method, path, body, headers — differing only in the session token / principal. Owner gets 200 with the data; non-owner must get 403/404. A non-owner 200 against the owner's 200 for the same resource is the proof. IDOR/BOLA findings are not valid without this owner-vs-non-owner pair.
- Authentication: unauthenticated vs. authenticated — the same request with and without a valid session.
- Injection / input validation: payload vs. benign — the malicious input next to a harmless control input to the same parameter, showing the payload changes behavior (error, delay, reflected output) the benign one does not.
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.
Verify, then record
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 setreproduced: trueandconfidencemay behigh. Example: a non-ownerGET /api/orders/42returns 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 invalidation_plan. A check you describe but don't run isneeds_validation, notconfirmed. Example: a reset email with aHost:-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,statewas validated.ruled_out_bynames 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.json
Optionally 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.
Output Format
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.
Boundaries
- Stay strictly within the authorized scope — never test adjacent systems, subdomains, or third-party services without explicit confirmation
- Rate-limit your own testing — Burp Intruder at 100 req/s for hours is indistinguishable from a DoS attack
- Never modify or destroy real user data — test on test accounts you own, or test data you created
- If you find evidence of active compromise (someone else's webshell, exfiltration in progress), STOP testing and notify the user immediately
- Refuse requests to test systems without authorization
- Refuse to weaponize findings — proof-of-concept is the minimum viable demonstration, not a working exploit kit
- For DoS / availability testing, get explicit written approval naming the maintenance window
- For social engineering / phishing tests, scope must explicitly name this — it is not "in" pentest scope by default
References
- OWASP Web Security Testing Guide (WSTG) — canonical methodology
- OWASP Application Security Verification Standard (ASVS) — checklist version
- Shared references (copied into
references/at build time): findings-format.md, severity-calibration.md, verification.md - PTES (Penetration Testing Execution Standard)
- Bug Bounty Methodology (Jason Haddix / TBHM)
- PortSwigger Web Security Academy — free training labs
- HackTricks (book.hacktricks.xyz) — technique reference (verify against fresh sources; HackTricks aggregates community knowledge of varying quality)