CSRF proof-of-concept
Every Pentestas-confirmed CSRF finding ships with a downloadable, self-contained HTML proof-of-concept the operator can hand directly to a customer. Open the file in a browser already authenticated to the target — the PoC fires a cross-origin request, demonstrating the vulnerability without any further setup.
Why this matters
"We found CSRF on /transfer" is a low-impact bullet on a report. "Open this HTML file while logged in to your bank — your account just sent $1 to an attacker" is a customer conversation. Pentestas generates the latter automatically, only when it's been content-confirmed exploitable.
Five PoC templates
The renderer picks the smallest template that reproduces the vulnerable request:
- Form — auto-submitting
application/x-www-form-urlencodedform. The classic CSRF demo. - Multipart — auto-submitting
multipart/form-dataform for endpoints that require a multipart body. - JSON — cross-origin
fetch()with the simple-requesttext/plaintrick (no preflight). Falls back to a realContent-Type: application/jsonprobe to detect lax CORS. - Img — hidden
<img src>for GET-based state changes (logout, like, follow). No JS needed. - Link — manual-click
<a>for redirect-only flows.
The exploitability gate
A CSRF detection alone is not enough to ship a PoC. The renderer runs a five-gate verifier first; the PoC ships only when every required gate passes. Failing chains still emit the finding, but with no PoC and a near-miss audit trail in poc_gate_reasons.
- G1 — cookie auth in use. Endpoints that authenticate via
Authorization: Beareralone aren't CSRF-able from a third-party site (the browser doesn't carry the bearer cross-origin). Cookie auth must be present. - G2 — SameSite is not Strict. A
SameSite=Strictcookie is refused on cross-site navigations. Lax / None / missing → the cookie travels; the gate passes. - G3 — no enforced custom header. If the server requires
X-Requested-With,X-CSRF-Token, or similar, the gate replays without the header. If the server's response diverges, the header is enforcing → the CSRF isn't cross-origin-exploitable. The replay actually strips the header from the persistent client (passingheaders={}doesn't, becausehttp_clientmerges defaults). - G4 — endpoint is auth-gated. The gate replays without the session cookie and verifies the body / status diverges from the cookied replay. An endpoint that returns the same response with or without auth isn't actually authenticated → CSRF is moot.
- G5 — mutation echo (advisory). The bypass response carried positive evidence of a state change: redirect to a thank-you page, success phrase,
"created":true, etc. When G5 fails the PoC still ships, but at MEDIUM rather than HIGH.
Downloading the PoC
Open a CSRF finding's drawer in the SPA. Findings that carry a PoC show a Proof of Concept section with:
- The PoC kind (form / multipart / json / img / link).
- An EXPLOITABLE badge (red) when all required gates passed; unverified (orange) when the PoC was generated but a gate failed advisory G5.
- A Download PoC HTML button.
- An expandable "Exploitability gate details" section listing every G1–G5 outcome.
The download endpoint is tenant-scoped:
curl -H "Authorization: Bearer $PT_TOKEN" \
-o csrf-poc.html \
"https://app.pentestas.com/api/findings/$FINDING_ID/csrf-poc"
Response carries Content-Disposition: attachment + X-Content-Type-Options: nosniff; the browser saves the file rather than rendering it (rendering would auto-fire the PoC against whichever app the operator is logged in to).
Safety guards baked into every PoC
- No cookies are ever embedded. The browser supplies the victim's cookies at runtime — the PoC must never carry server-side session state. Same file works for any victim with no operator wiring.
- The PoC announces itself. Every template renders a yellow banner identifying the file as a Pentestas-generated PoC, citing the scan id and the bypass technique. Customers tend to forward these internally — the banner stops them from being mistaken for live attacks.
- URL scheme guard. The renderer rejects
javascript:,data:,vbscript:,file:,ftp:, and any URL without a host. A scanned target with a form action ofjavascript:badness()would otherwise XSS the operator's own browser when they open the downloaded HTML; the guard makes that impossible. - JSON template escaping. Parameter values containing
</script>are escaped to<\/script>before embedding — no premature script-tag termination, even when the captured form value is a payload itself.
Encryption at rest
The generated PoC HTML embeds the target URL plus operator-supplied parameter values. It's stored encrypted with the tenant's Fernet key alongside evidence and payload_used. The download endpoint decrypts on the fly and never persists the plaintext.
When no PoC attaches
Findings emit normally even when the gate failed; the operator gets has_poc: false and a populated poc_gate_reasons array explaining which gate(s) blocked emission. Common shapes:
G1_cookie_auth: failed— the target uses bearer-token auth.G2_samesite_not_strict: failed— every session cookie hasSameSite=Strict.G3_no_custom_header: failed— server enforcesX-Requested-With; the response diverged when the header was stripped.render_blocked_unsafe_url: ...— the form action wasjavascript:/data:/ relative; PoC rendering would have been unsafe.
See also
- Exploitation chains — the broader orchestrator that demonstrates impact for non-CSRF findings
- Validation — the Accuracy Gate that guards every CRITICAL/HIGH finding
- Severity scale