Practical Web Security for Frontend Developers: CSRF, XSS, and CSP

Team 6 min read

#frontend-security

#csrf

#xss

#csp

CSRF: What it is and why it matters

Cross-Site Request Forgery (CSRF) tricks a user’s browser into making unintended state-changing requests to a web application where the user is authenticated. The browser automatically attaches cookies, so the site may perform actions without the user’s knowledge or intent.

Key points:

  • CSRF is a trust issue: the site trusts the user’s browser, which may be influenced by another site.
  • State-changing actions (like transferring funds or changing account details) are the primary risk.
  • APIs that rely on cookies for authentication are especially vulnerable if protections aren’t in place.

Defending against CSRF in modern apps

Practical defenses you can apply in frontend-centric workflows:

  • Use same-site cookies (SameSite) for session cookies:

    • SameSite=Lax or SameSite=Strict helps prevent unauthorized cross-origin requests.
    • Note: This is a server-side attribute, but it directly affects what your frontend can rely on.
  • Prefer tokens for state-changing requests:

    • Use CSRF tokens for endpoints that mutate state. The server issues a token per user session, and the frontend must send it with mutating requests (often in a header or form field).
    • Double-submit cookie pattern: send a CSRF token in both a cookie and a request header and verify they match on the server.
  • Use a safe authentication pattern for APIs:

    • If possible, use Authorization headers (Bearer tokens or similar) instead of cookies for API calls. This reduces CSRF risk because cross-origin requests won’t automatically attach your tokens.
  • Validate the Origin or Referer headers for sensitive actions:

    • A server-side check can help, but write this as a defense-in-depth layer rather than the sole protection.
  • Avoid performing significant state changes with GET requests:

    • RESTful design and proper HTTP methods reduce risk exposure.
  • For forms and fetch requests from frontend:

    • Include a CSRF token in headers or as a hidden form field and validate it server-side.

Example (frontend-centric pattern using a CSRF token in a header):

  • The server issues a per-session CSRF token.
  • The frontend reads the token (e.g., from a meta tag or a cookie, whichever you align with).
  • All mutating requests include the header X-CSRF-Token: .

Code sketch:

// Example: include CSRF token in a fetch request
async function submitForm(data) {
  const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
  const res = await fetch('/api/transfer', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': token
    },
    credentials: 'include', // if cookies are needed for auth
    body: JSON.stringify(data)
  });
  return res.json();
}

XSS: Understanding types and prevention

Cross-Site Scripting (XSS) occurs when an attacker injects malicious scripts into content that other users view. There are several flavors:

  • Reflected XSS: payload appears in the immediate response of a request (e.g., via URL parameters).
  • Stored XSS: payload is stored on the server (e.g., in a database) and delivered to users.
  • DOM-based XSS: payload is processed entirely on the client side and modifies the DOM without server reflection.

Core defenses:

  • Escape and encode output by default:

    • Always escape user-supplied data before rendering in HTML, attribute values, and URLs.
    • Rely on your framework’s built-in escaping rather than ad-hoc string concatenation.
  • Use safe rendering practices:

    • Avoid inserting HTML with innerHTML unless the content is sanitized.
    • Prefer text content rendering for user-supplied data.
  • Sanitize user input when necessary:

    • Use a reputable sanitizer library (e.g., DOMPurify) for content that must be rendered as HTML.
  • Reduce reliance on inline scripts:

    • Place scripts in external files and avoid inline event handlers.
  • Apply a strong Content Security Policy (CSP) to mitigate XSS:

    • CSP helps reduce the impact of any injected script by restricting what can run and from where.

Code example: basic DOM sanitizer usage

<!-- Include a sanitizer library -->
<script src="https://cdn.jsdelivr.net/npm/dompurify@2.4.0/dist/purify.min.js"></script>

<div id="comment"></div>
<script>
  const userInput = '<img src=x onerror="alert(1)">';
  // Safe rendering
  document.getElementById('comment').textContent = userInput;
  // If you must render HTML, sanitize first
  const safe = DOMPurify.sanitize(userInput);
  document.getElementById('comment').innerHTML = safe;
</script>

Guidelines for frontend frameworks:

  • React, Vue, and other modern frameworks escape by default when rendering user data.
  • Avoid dangerouslySetInnerHTML or equivalent unless you sanitize thoroughly and control the content source.

Content Security Policy (CSP)

CSP is a powerful, server-enforced mechanism to reduce XSS risk and other injection attacks by restricting where content can be loaded from and which scripts can run.

Key concepts:

  • Default-src controls the default sources for content.
  • script-src and style-src restrict script and style sources; you can use nonces or hashes to allow trusted inline scripts/styles.
  • Nonces: server generates a unique nonce per response; inline scripts/styles must include the matching nonce.
  • Hashes: inline scripts/styles can be allowed if their content matches a cryptographic hash.
  • report-to or report-uri lets you collect CSP violation reports for monitoring.

A robust starter policy: Content-Security-Policy: default-src ‘self’; script-src ‘self’ https://trusted-cdn.example.com ‘nonce-’; style-src ‘self’ ‘nonce-https://fonts.googleapis.com; img-src ‘self’ data:; connect-src ‘self’; font-src ‘self’ https://fonts.gstatic.com; object-src ‘none’; base-uri ‘self’; form-action ‘self’; frame-ancestors ‘none’; report-to CSPReportService;

Notes:

  • Replace with a server-generated nonce for each response.
  • Avoid ‘unsafe-inline’ unless absolutely necessary; prefer nonces or hashes.
  • Use a reporting endpoint (e.g., CSPReportService) to monitor violations and adapt policies.

Example of a minimal CSP header using a nonce:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-ABC123'; style-src 'self' 'nonce-ABC123'; object-src 'none'; report-to CSPReportService

How to adopt CSP gradually:

  • Start with a report-only mode to observe what would be blocked.
  • Once you identify dependencies, tighten the policy and supply nonces or hashes for trusted inline code.
  • Apply CSP on both static assets and dynamic content, updating server configurations accordingly.

Practical checklist for frontend developers

  • Use a modern framework that escapes content by default.
  • Always escape or sanitize user-generated content before rendering.
  • Prefer external scripts and styles; minimize inline code.
  • Implement a strict CSP with nonces or hashes; enable report-only mode first.
  • Use SameSite cookies for session cookies; prefer Authorization headers for APIs when possible.
  • Implement CSRF protections for state-changing endpoints that rely on cookies.
  • Validate the presence and correctness of anti-CSRF tokens on the server side.
  • Sanity-check Referer and Origin headers for sensitive actions.
  • Regularly review dependencies for security updates, especially libraries that render or sanitize HTML.
  • Strong CSP with nonce example (replace at runtime): Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-https://trusted-cdn.example.com; style-src ‘self’ ‘nonce-https://fonts.googleapis.com; img-src ‘self’ data:; connect-src ‘self’; font-src ‘self’ https://fonts.gstatic.com; object-src ‘none’; report-to CSPReportService

  • Using fetch with credentials for cookie-based sessions: fetch(‘/api/endpoint’, { method: ‘POST’, credentials: ‘include’, headers: { ‘X-CSRF-Token’: '' }, body: JSON.stringify({ /* data */ }) })

  • Simple CSP violation reporting (server collects reports): Content-Security-Policy-Report-Only: default-src ‘self’; report-uri /csp-report

Summary

Frontend security hinges on defense in depth:

  • Mitigate CSRF for state-changing operations through tokens, safe cookie attributes, and careful API design.
  • Prevent XSS by escaping data, sanitizing where necessary, and adopting secure rendering practices.
  • Enforce a robust CSP to minimize the impact of any injection and provide a clear policy for what the browser should allow.

By integrating these practices into your development workflow, you can significantly reduce the attack surface exposed by modern frontend applications.