Pentestas / help

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-urlencoded form. The classic CSRF demo.
  • Multipart — auto-submitting multipart/form-data form for endpoints that require a multipart body.
  • JSON — cross-origin fetch() with the simple-request text/plain trick (no preflight). Falls back to a real Content-Type: application/json probe 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: Bearer alone 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=Strict cookie 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 (passing headers={} doesn't, because http_client merges 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
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 of javascript: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 has SameSite=Strict.
  • G3_no_custom_header: failed — server enforces X-Requested-With; the response diverged when the header was stripped.
  • render_blocked_unsafe_url: ... — the form action was javascript: / data: / relative; PoC rendering would have been unsafe.

See also