into every input field, URL parameter, and search box in your app, then look for a dialog box appearing anywhere. Also check for the string appearing unencoded in the page source. For a thorough automated scan, run OWASP ZAP (free) against your app in active scan mode \u2014 it specifically checks for reflected and stored XSS across all discovered inputs." } } ] }
Home System Design CSRF and XSS Prevention: How to Actually Secure Your Web App

CSRF and XSS Prevention: How to Actually Secure Your Web App

In Plain English 🔥
Imagine you're logged into your bank in one browser tab. A hacker tricks you into clicking a link in another tab — and your browser silently sends your bank a 'transfer money' request using your real login cookie, without you knowing. That's CSRF. XSS is different: it's like someone sneaking a fake note onto a school bulletin board that says 'tell me your locker combination' — the school's own wall is delivering the attacker's message to every student who reads it. Both attacks abuse trust: CSRF abuses the server's trust in your browser, and XSS abuses the user's trust in your website.
⚡ Quick Answer
Imagine you're logged into your bank in one browser tab. A hacker tricks you into clicking a link in another tab — and your browser silently sends your bank a 'transfer money' request using your real login cookie, without you knowing. That's CSRF. XSS is different: it's like someone sneaking a fake note onto a school bulletin board that says 'tell me your locker combination' — the school's own wall is delivering the attacker's message to every student who reads it. Both attacks abuse trust: CSRF abuses the server's trust in your browser, and XSS abuses the user's trust in your website.

Every year, companies lose millions of dollars and millions of user records because of two vulnerabilities that have been fully understood since the early 2000s — and are still getting exploited in production apps today. CSRF (Cross-Site Request Forgery) and XSS (Cross-Site Scripting) sit near the top of the OWASP Top 10 list not because they're exotic or new, but because developers keep underestimating them. A misconfigured form, a single unescaped variable, or one missing HTTP header is all it takes.

These attacks aren't theoretical. The 2018 British Airways breach that exposed 500,000 customers' payment details was traced to a form of XSS. CSRF has been used to hijack router settings, drain user accounts, and post content without consent. What makes them dangerous isn't complexity — it's how easy they are to miss when you're moving fast.

By the end of this article you'll understand exactly how each attack works at the HTTP level, why the standard defenses actually stop them, how to implement those defenses in real Node.js/Express and Python/Django code, and — critically — what common shortcuts create a false sense of security. You'll be able to review your own codebase and spot the gaps.

How CSRF Actually Works — and Why Cookies Are the Root Cause

To understand CSRF, you need to understand one browser behavior that most developers take for granted: browsers automatically attach cookies to every request made to a domain, regardless of which site triggered that request.

So if you're logged into bank.com and have a session cookie, and you visit evil.com, that malicious page can fire a form POST to bank.com/transfer — and your browser will attach your session cookie to that request automatically. The bank's server sees a valid session and processes the transfer. You never clicked anything on the bank's site.

The attack works because the server can't distinguish between a legitimate request (you clicking a button on bank.com) and a forged request (a form on evil.com targeting bank.com). They look identical at the HTTP level.

The CSRF token pattern defeats this because it introduces a secret that evil.com cannot know. The server embeds a random, unpredictable token in the legitimate page's form. When the form is submitted, the server checks that the token matches. Since evil.com can't read the content of bank.com's pages (same-origin policy blocks that), it can't forge the token. No valid token means the request is rejected — even if the session cookie is attached.

csrf_protection_express.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// npm install express express-session csurf cookie-parser
// This shows a complete Express app with CSRF protection on a state-changing route.

const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');

const app = express();

// Parse URL-encoded form bodies (needed to read the CSRF token from POST body)
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

// Session middleware — CSRF tokens are tied to a session
app.use(session({
  secret: process.env.SESSION_SECRET || 'replace-with-strong-random-secret',
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,   // JS cannot read this cookie
    secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
    sameSite: 'strict' // First line of CSRF defense — see callout
  }
}));

// csrfProtection middleware — generates and validates tokens
const csrfProtection = csrf({ cookie: false }); // store token in session, not a separate cookie

// GET /transfer — render the transfer form, embed CSRF token in hidden field
app.get('/transfer', csrfProtection, (req, res) => {
  const csrfToken = req.csrfToken(); // generate a unique token for this session

  // In a real app you'd use a template engine (EJS, Handlebars, etc.)
  // For clarity, we're sending raw HTML here
  res.send(`
    <form method="POST" action="/transfer">
      <!-- The CSRF token is hidden from the user but included in every POST -->
      <input type="hidden" name="_csrf" value="${csrfToken}" />
      <label>Amount: <input type="number" name="amount" /></label>
      <label>To Account: <input type="text" name="toAccount" /></label>
      <button type="submit">Transfer</button>
    </form>
  `);
});

// POST /transfer — csrfProtection middleware will reject requests with missing/invalid token
app.post('/transfer', csrfProtection, (req, res) => {
  const { amount, toAccount } = req.body;

  // If we reach here, the CSRF token was valid — safe to process
  console.log(`Processing transfer of $${amount} to account ${toAccount}`);
  res.send(`Transfer of $${amount} to ${toAccount} completed successfully.`);
});

// CSRF error handler — fires when the token is missing or doesn't match
app.use((err, req, res, next) => {
  if (err.code === 'EBADCSRFTOKEN') {
    // This is what happens when evil.com tries to POST without a valid token
    console.warn('CSRF attack detected or invalid token — request blocked');
    return res.status(403).json({
      error: 'Invalid or missing CSRF token. Request blocked.'
    });
  }
  next(err);
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));
▶ Output
Server running on http://localhost:3000

// When a legitimate form is submitted:
Processing transfer of $500 to account 9876543
// Response: "Transfer of $500 to 9876543 completed successfully."

// When evil.com tries to POST without the token:
CSRF attack detected or invalid token — request blocked
// Response (HTTP 403): { "error": "Invalid or missing CSRF token. Request blocked." }
⚠️
SameSite Cookies — Your Free First Line of DefenseSetting your session cookie to SameSite=Strict or SameSite=Lax stops most CSRF attacks before the token even matters, because the browser won't send the cookie at all on cross-site requests. Use both — SameSite as your first barrier and CSRF tokens as the bulletproof second layer. Never rely on SameSite alone; older browsers don't support it, and Lax still allows GET-based CSRF.

How XSS Works — and Why 'Escaping Output' Is Non-Negotiable

XSS happens when your application takes untrusted data — from a user, a URL parameter, a database — and renders it in a browser as executable HTML or JavaScript, instead of as plain text.

There are three types. Reflected XSS: the malicious script is in the URL and reflected back in the response (e.g., a search page that prints 'You searched for: [input]' without escaping). Stored XSS: the payload is saved to the database and served to every user who views that page — far more dangerous. DOM-based XSS: the injection happens entirely in JavaScript, never touching the server, when client-side code writes untrusted data to innerHTML.

The fix is to treat user input as data, never as markup. This means HTML-encoding special characters before rendering them. < becomes <, > becomes >, " becomes ", and so on. A into every input field, URL parameter, and search box in your app, then look for a dialog box appearing anywhere. Also check for the string appearing unencoded in the page source. For a thorough automated scan, run OWASP ZAP (free) against your app in active scan mode — it specifically checks for reflected and stored XSS across all discovered inputs.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

About Our Team Editorial Standards
← PreviousDomain-Driven Design BasicsNext →Encryption at Rest and in Transit
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged