Three attacks with three different targets
XSS, CSRF and SSRF all abuse trust, but each one targets a different party. Cross-site scripting (XSS) runs an attacker's script in your users' browsers. Cross-site request forgery (CSRF) makes a signed-in user's browser send a request they didn't intend. Server-side request forgery (SSRF) makes your server fetch a URL the attacker chooses.
Each needs its own defense. Fixing one doesn't cover the others.
How cross-site scripting works
XSS happens when untrusted data reaches a page in a form the browser treats as code. The injected script runs with your page's origin, so it can read what the user sees, change the page and send requests as the user.
- Stored XSS: the payload is saved, for example in a comment or profile field, and served to everyone who views it.
- Reflected XSS: the payload comes from the request, often a URL parameter, and is echoed back in the response. The attacker has to get the victim to open a crafted link.
- DOM-based XSS: client-side JavaScript takes attacker-controlled data, such as location.hash, and writes it into a dangerous sink like innerHTML. The payload may never reach the server.
Preventing XSS
The main defense is contextual output encoding: escape data for the exact place it lands. HTML text, HTML attributes, JavaScript strings and URLs each need different encoding.
- Use templates or frameworks that escape by default, such as JSX text in React, and review every raw-HTML escape hatch such as dangerouslySetInnerHTML.
- When users must submit rich HTML, clean it with a maintained sanitizer such as DOMPurify instead of a home-made filter.
- Prefer textContent over innerHTML, and allow only http and https in user-supplied links so javascript: URLs cannot run.
- Add a Content-Security-Policy as a second layer. A nonce-based policy can stop an injected script from running when an encoding bug slips through.
- Mark session cookies HttpOnly so injected scripts cannot read them, keeping in mind that the script can still act as the user.
Cross-site request forgery and its defenses
CSRF works because a browser can attach a user's cookies to a request even when another site started it. A hidden form on the attacker's page posts to your app, the session cookie goes along, and the request looks legitimate to your server.
It matters for state-changing actions, such as changing an email address or sending a payment. The attacker can't read the response, because the same-origin policy still blocks that. Layer these defenses:
- Use your framework's built-in CSRF protection where it has one.
- Require an anti-CSRF token on every state-changing request: a random value tied to the session that another site can't read or guess.
- Check the Origin header, or Sec-Fetch-Site, on state-changing requests and reject origins you do not expect.
- Set session cookies to SameSite=Lax or Strict as an extra layer. SameSite treats all subdomains of your registrable domain as the same site, so it does not stop a request from a compromised subdomain.
- Never change state on GET requests, because SameSite=Lax still sends cookies on top-level GET navigation.
Server-side request forgery and its defenses
SSRF appears wherever your server fetches a URL that a user supplied, such as link previews, webhooks, image imports and PDF renderers. The attacker points the fetch at addresses only your server can reach, such as 127.0.0.1 or an internal admin panel. Another target is the link-local cloud metadata endpoint at 169.254.169.254, which on some cloud setups returns temporary credentials.
Checking the URL string isn't enough, because a hostname can resolve to a private address and a redirect can lead anywhere. Add checks at each step of the fetch:
- Allow only the destinations the feature needs, by domain or exact URL, whenever you can.
- Accept only the http and https schemes.
- Resolve the hostname and reject private, loopback and link-local addresses, such as 10.0.0.0/8, 127.0.0.0/8 and 169.254.0.0/16, plus their IPv6 equivalents.
- Connect to the IP address you validated, so a second DNS answer cannot swap the target (DNS rebinding).
- Turn off automatic redirects and re-validate every hop before following it.
- Return neither the raw response nor detailed fetch errors to the user.