XSS & CSRF Protection
Preventing scripts from being injected into pages and cross-site requests from being forged.
Overview
Cross-Site Scripting (XSS) injects malicious scripts into web pages viewed by other users, enabling session theft, phishing, and keylogging. Cross-Site Request Forgery (CSRF) tricks authenticated users into submitting requests to a different site. XSS prevention requires output encoding and Content Security Policy; CSRF prevention requires synchronised tokens or SameSite cookie attributes.
Origin
XSS was named in a Microsoft Security Advisory in 2000, though the vulnerability existed earlier. CSRF was described by Peter Watkins in 2001. XSS worm Samy (2005) infected one million MySpace profiles in 20 hours. CSRF led to high-profile account takeovers at Twitter (2007) and Netflix (2006). Both feature in every OWASP Top 10 edition.
Examples
XSS prevention with Content Security Policy in TypeScript
import helmet from 'helmet';
import { Express } from 'express';
export function configureSecurityHeaders(app: Express): void {
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.trusted.com'],
styleSrc: ["'self'", "'unsafe-inline'"], // prefer hashes over unsafe-inline
imgSrc: ["'self'", 'data:', 'https://images.example.com'],
connectSrc: ["'self'", 'https://api.example.com', 'wss://ws.example.com'],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
objectSrc: ["'none'"],
frameAncestors: ["'none'"], // prevents clickjacking
upgradeInsecureRequests: [],
},
reportOnly: false, // enforce, not just report
})
);
app.use(helmet.xssFilter()); // X-XSS-Protection header for older browsers
}CSP blocks injected scripts even if they reach the DOM by restricting which script sources are trusted. frameAncestors: 'none' replaces the deprecated X-Frame-Options header. Report-only mode allows CSP testing without breakage before enforcement.
CSRF protection in Rails with Authenticity Token
# Rails CSRF protection is enabled by default via ActionController::RequestForgeryProtection
# application_controller.rb
class ApplicationController < ActionController::Base
# protect_from_forgery is included by default in Rails 5+
# It uses :exception strategy (raises InvalidAuthenticityToken) by default
protect_from_forgery with: :exception
# For API endpoints that use JWT (no session cookie), CSRF is not applicable
# Use :null_session to prevent CSRF checks for token-authenticated requests
# protect_from_forgery with: :null_session, if: :json_request?
end
# Separate API controller that disables CSRF for Bearer token auth
class Api::BaseController < ActionController::API
# ActionController::API does not include CSRF protection by default
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_api_token!
private
def authenticate_api_token!
authenticate_with_http_token do |token, _options|
@current_user = ApiToken.active.find_by(token: token)&.user
end
render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
end
endRails embeds the CSRF token in forms via form_authenticity_token and validates it on non-GET requests. Modern SPA architectures using JWTs in HttpOnly cookies with SameSite=Strict are CSRF-immune because cross-site requests cannot read the cookie value.
Use Cases
- 01Any application rendering user-generated content (comments, profiles, messages) must escape HTML output
- 02SPAs that call APIs must include CSRF tokens or rely on SameSite cookie protection and CORS headers
- 03Rich text editors (TipTap, Quill) that accept HTML input must sanitise with an allowlist-based sanitiser server-side
- 04Markdown renderers must sanitise the HTML output after parsing, not just the Markdown input
When Not to Use
- //Do not use innerHTML, document.write(), or eval() with user-supplied data; use textContent or DOM construction APIs instead
- //Do not mark a string as safe (html_safe in Rails, dangerouslySetInnerHTML in React) without running it through a sanitiser first
- //Do not disable CSRF protection for convenience on non-API routes; even if you think the action is harmless, attackers may chain CSRF with other vulnerabilities
Technical Notes
- DOM-based XSS occurs entirely in the browser; the payload never reaches the server. Sources include location.hash, document.referrer, and URL parameters. Sinks include innerHTML, eval(), and setTimeout with string arguments. CSP mitigates but server sanitisation does not prevent it
- React's JSX escapes all values before rendering by default; XSS requires explicitly opting into dangerouslySetInnerHTML or using third-party libraries that call innerHTML
- SameSite=Strict prevents the cookie from being sent on any cross-site request, providing strong CSRF protection. SameSite=Lax (the default in modern browsers) prevents cross-site POST but allows cross-site GET navigations
- Trusted Types (W3C proposal, implemented in Chrome 83+) enforces that only values created through Trusted Types policies can be assigned to dangerous sinks, providing a compile-time-like check for DOM XSS at runtime
More in Safety