Cross-site scripting and cross-site request forgery are the two browser-side attack classes developers most often confuse with each other. They share a "cross-site" prefix and they both abuse a logged-in user's relationship with a vulnerable application, but the mechanism, the payload, the prerequisite, and the defense are different in every other respect. The difference between csrf and xss matters because the defenses are largely orthogonal — a team that ships strong XSS prevention but forgets CSRF tokens has covered half the surface, and a team that adds SameSite cookies without auditing the rendering layer is no safer against an injected <script>. This guide is the side-by-side comparison: cross site request forgery vs cross site scripting, the 30-second difference, the attack flow for each, the dangerous combination where XSS bypasses CSRF tokens, a row-by-row comparison table, the defenses each requires, and the closing reminder that secure architectures defend against XSS, CSRF, and SQL injection together rather than treating any one as solved.
The 30-Second Difference Between CSRF and XSS
The shortest accurate framing of cross site request forgery vs cross site scripting is that they exploit different sides of the same trust relationship. The browser trusts the application's origin (cookies, localStorage, same-origin reads) and the application trusts the user's session (the cookie or token presented with a request is treated as proof the request came from the user). XSS attacks the first trust: the attacker injects script that runs in the application's origin and inherits the same trust the browser grants the application, so the script can read cookies, exfiltrate session data, and call same-origin endpoints with the user's credentials. CSRF attacks the second trust: the attacker never injects code into the application; instead, the attacker hosts a different origin that tricks the user's browser into sending an authenticated request to the vulnerable application, riding the user's existing session cookie to perform a state-changing action the user did not intend.
Reduced to one sentence each: XSS = inject code that runs in the victim's browser inside the application's origin; CSRF = ride the victim's authenticated session from a different origin to perform a state-changing action. Every other distinction — the payload, the prerequisite, the detection signature, the defense — follows from that single difference. Xss vs csrf is not "two flavors of the same bug"; it is two structurally different attacks against two structurally different parts of the browser security model.
XSS Attack Flow — One-Line Recap
Cross-site scripting begins when an application combines untrusted data with HTML or JavaScript that the browser will parse, and ends when the resulting markup contains attacker-controlled script that runs in the application's origin. The attacker controls the payload's text — a <script> tag, an <img onerror>, a javascript: URI, a DOM-mutating innerHTML assignment — and the rendering surface decides whether that text reaches the browser as text or as code. When it reaches the browser as code, the script runs with the application's origin privileges: it can read document.cookie (unless HttpOnly is set), read DOM nodes, call same-origin endpoints with the user's credentials, and exfiltrate any data it can reach. The full taxonomy — reflected, stored, DOM-based, mutation, blind — lives in our cross-site scripting pillar guide and the dedicated XSS prevention defense-in-depth guide. For this comparison the relevant detail is the execution context: an XSS payload runs in the victim's browser, in the application's origin. That execution context is what makes XSS the more powerful of the two attacks — and what makes XSS, when it lands, capable of bypassing CSRF defenses entirely.
CSRF Attack Flow — Riding the Authenticated Session
Cross-site request forgery begins when a user, already logged in to a vulnerable application, visits a different origin controlled by the attacker. The attacker's page contains a hidden form, an auto-submitting JavaScript snippet, an <img> with a state-changing URL, or any other element that triggers the browser to send a request to the vulnerable application's endpoint. The user's browser, following its standard same-target cookie behavior, attaches the session cookie for the vulnerable application's origin to that request. The vulnerable application receives the request, sees a valid session cookie, and treats the request as legitimate — it executes the state-changing action (transferring funds, changing the email address, deleting the account, granting an admin role) on behalf of the user, even though the user never intended to make the request.
The mechanism depends on a single property of cookie-based authentication: cookies are sent automatically with cross-origin requests by default. The attacker does not need to read the cookie, does not need to know its value, and does not need to inject any code into the vulnerable application. The browser sends the cookie itself, and the application's request handler treats the resulting authenticated request as legitimate. The attacker is firing requests blind, hoping the application performs the action and trusting the browser's ambient cookie behavior to provide authentication.
The classic CSRF target is a state-changing endpoint reachable via a form-encoded POST that the attacker can construct without same-origin restrictions. A bank transfer endpoint that accepts POST /transfer with form-encoded amount and destination is the textbook target: the attacker hosts a page with an auto-submitting form pointing at /transfer, the user visits the page while logged in to the bank, the form submits, the browser sends the session cookie, and the transfer happens. Modern applications have largely moved to JSON request bodies and custom headers, which raises the bar (a cross-origin fetch with a custom header triggers a CORS preflight that the attacker's origin cannot pass), but applications that still accept form-encoded state-changing requests without CSRF protection remain widely exploitable.
The Dangerous Combination — When XSS Bypasses CSRF Tokens
The most common defense against CSRF is the synchronizer token: the application embeds a per-session, unguessable token in every form, the browser submits it with the request, and the application rejects any state-changing request without a matching token. The attacker, hosting a different origin, cannot read the token (same-origin policy prevents the attacker's page from reading the vulnerable application's DOM), so the attacker cannot construct a valid forged request. CSRF is mitigated.
The mitigation depends on the same-origin policy holding. When XSS lands on the vulnerable application, the same-origin policy no longer protects the token: the attacker's script runs in the application's origin, can read any DOM node, and can read the CSRF token directly. The script then constructs a valid forged request — same token, same cookie, same headers — and submits it. The CSRF defense fails because the attacker is no longer on a different origin; the attacker's script is running on the vulnerable origin via XSS.
The sequence below shows the exfiltrate-and-submit pattern in compact form. An XSS payload, once it has landed in the page, can read the CSRF token from a meta tag, hidden input, or cookie, and submit a forged state-changing request that the application accepts as legitimate:
// XSS payload running in the application's origin reads the
// CSRF token from a meta tag and submits a forged transfer request.
// Same-origin: the script can read the token. Cookie travels
// automatically with same-origin fetch. The CSRF defense, which
// assumed the attacker was cross-origin, fails by construction.
const csrfToken = document
.querySelector('meta[name="csrf-token"]')
.getAttribute('content');
await fetch('/admin/transfer', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': csrfToken,
},
body: new URLSearchParams({
csrf_token: csrfToken,
amount: '50000',
destination: 'attacker-account-1234',
}),
});
// The server validates the token, validates the session cookie,
// and processes the transfer. From the server's perspective, the
// request is indistinguishable from a legitimate user action.CSRF tokens, double-submit cookies, custom headers, and Origin/Referer checks all assume the attacker cannot read the application's DOM or set arbitrary headers from the application's origin. XSS breaks every one of those assumptions. The implication is not that CSRF defenses are useless — they remain critical against the cross-origin attack class they were designed to stop — but that CSRF defense is contingent on no XSS landing in the same origin. A team that has rigorous CSRF protection but a single stored XSS in a comment field has effectively no CSRF protection for any user who views that comment. This is why XSS is rated higher severity than CSRF in most rubrics, and why the layered defense applies both controls rather than treating either as sufficient.
Side-by-Side Comparison Table
The row-by-row breakdown below maps the differences between XSS and CSRF onto the dimensions developers reach for in code review and threat modeling. The table is the fastest way to see the difference between cross site scripting and cross site request forgery at a glance.
| Dimension | XSS | CSRF |
|---|---|---|
| Vector | Untrusted data rendered as HTML/JS in the application's origin. | Cross-origin request that rides the user's session cookie. |
| Payload | Attacker-controlled script: <script>, onerror=, innerHTML, etc. | A forged HTTP request — no script executes on the attacker's side. |
| Where it runs | In the victim's browser, in the application's origin. | In the application's server-side request handler. |
| Prerequisite | Application renders untrusted data without proper encoding. | Application accepts state-changing requests authenticated only by ambient cookies. |
| Attacker reads response? | Yes — script runs same-origin and can read the DOM. | No — same-origin policy blocks the attacker's page from reading the response. |
| Primary defense | Context-aware output encoding, framework auto-escape, CSP, Trusted Types. | CSRF tokens, SameSite cookies, custom headers, Origin/Referer checks. |
| Cookie attribute that helps | HttpOnly (blocks document.cookie exfiltration). | SameSite=Lax or Strict (blocks cross-site cookie attachment). |
| Detection signature | Reflected payloads in responses; DOM-XSS source-to-sink flow. | State-changing requests without anti-forgery token; missing Origin checks. |
| Severity (typical) | High — full session compromise in origin. | Medium-High — bounded by which state-changing endpoints are reachable. |
| OWASP Top 10 home | A03 (Injection) since 2021. | A01 (Broken Access Control) since 2021; previously its own category. |
Two rows are worth pausing on. "Attacker reads response" explains why XSS is the more powerful attack: an XSS payload can exfiltrate any data the application's origin exposes, while a CSRF attack can only fire and forget a state change. "Cookie attribute that helps" highlights the orthogonality of the defenses: HttpOnly shrinks XSS impact by blocking session-token theft but does nothing against CSRF; SameSite blocks the cross-origin cookie attachment CSRF depends on but does nothing against XSS. Both attributes belong on session cookies, and neither replaces the other.
XSS Defense Is Not CSRF Defense
The orthogonality of the two defenses is the most important practical takeaway from the csrf vs xss comparison. Output encoding does not stop CSRF. SameSite cookies do not stop XSS. CSP does not stop CSRF. CSRF tokens do not stop XSS. Each defense addresses one class, and a complete posture applies both.
CSRF tokens (synchronizer pattern). The application generates a per-session unguessable token, embeds it in every form, and validates the submitted token against the session-stored value. A request without a matching token is rejected. Modern frameworks ship this pattern as default middleware: Django's CsrfViewMiddleware, Rails's protect_from_forgery, ASP.NET's anti-forgery filter, Spring Security's CSRF support, Laravel's VerifyCsrfToken. The discipline is to never disable the default middleware globally and to never bypass it for state-changing endpoints.
Double-submit cookie. An alternative when stateless backends cannot store per-session tokens. The application sets a CSRF cookie containing a random value, and the client-side code copies the cookie's value into a request header or body field on every state-changing request. The server compares the header value to the cookie value and rejects mismatches. The attacker, sitting on a different origin, cannot read the cookie's value (cross-origin script cannot read another origin's cookies), so cannot reproduce it in the header. Pairs naturally with single-page architectures that use fetch with custom headers.
SameSite cookies. The browser-level mitigation that matters most in 2026. The SameSite attribute on a cookie tells the browser when to attach it to cross-site requests. Three values: Strict (never sent on any cross-site request, even top-level navigations from external links), Lax (sent only on top-level GETs from cross-site contexts — links and bookmarks work, but cross-site POSTs, iframes, and image requests do not carry the cookie), and None (sent on all cross-site requests; requires the Secure attribute, appropriate only for cookies that must work in cross-site embedding). Most session cookies should be SameSite=Lax at minimum; Strict is preferred where the application does not need cross-site link-arrival to retain the session. Modern browsers default cookies without an explicit SameSite attribute to Lax, which has eliminated the simplest CSRF vector across most of the web.
The configuration below shows a session cookie with the attributes that matter — HttpOnly for XSS impact reduction, Secure for transport, SameSite=Lax for the CSRF baseline:
// Express + cookie-session example: the four attributes that
// every session cookie should set in 2026. Missing any one of
// these is a finding in code review.
app.use(session({
name: 'sid',
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true, // JavaScript cannot read document.cookie for this cookie
secure: true, // cookie sent only over HTTPS
sameSite: 'lax', // cookie not attached to cross-site POSTs/iframes/images
maxAge: 1000 * 60 * 60 * 8, // 8h session
},
}));
// The Set-Cookie header the browser receives:
// Set-Cookie: sid=...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=28800Custom request headers. A defense that complements CSRF tokens: the application requires a custom header (X-CSRF-Token, X-Requested-With) on state-changing requests. The browser's CORS rules require a preflight for any request with custom headers, and the attacker's cross-origin page cannot pass the preflight without the application's permission. The custom-header requirement turns CSRF from a "fire and forget form" attack into one that requires CORS bypasses, which dramatically narrows the exploit surface.
Origin and Referer header checks. The simplest server-side check: the application looks at the Origin header (set by the browser on cross-origin requests) and rejects any state-changing request whose origin does not match the application's allowed origins. The Referer header is the older fallback when Origin is absent. Both can be missing or stripped in some legitimate cases, so neither is a sole defense, but as a layered check they catch the majority of CSRF without breaking legitimate traffic. Connects to the broader session-and-cookie discipline covered in our OWASP A07 authentication failures guide.
The pair below shows a classic vulnerable form alongside the fixed version with synchronizer token and double-submit cookie:
<!-- Vulnerable: form has no CSRF token, accepts cross-origin POST -->
<form method="POST" action="/account/email">
<input type="email" name="email" />
<button type="submit">Update Email</button>
</form>
<!-- An attacker hosts a page at evil.example with the same form,
auto-submitting on load. The user, logged in elsewhere, visits
the attacker page; the form submits to /account/email with the
session cookie attached; the email change happens. --><!-- Fixed: synchronizer token + double-submit cookie pattern -->
<form method="POST" action="/account/email">
<input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
<input type="email" name="email" />
<button type="submit">Update Email</button>
</form>
<!-- Server side: validate that the submitted csrf_token matches
the session-stored token AND matches a CSRF cookie value (double
submit). Reject the request if either check fails. Also enforce
SameSite=Lax on the session cookie so the browser does not
attach it to cross-site POSTs in the first place. -->
<script>
// Single-page apps that use fetch should attach the token as a
// custom header. The custom header triggers a CORS preflight
// that cross-origin attackers cannot pass.
fetch('/account/email', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
},
body: JSON.stringify({ email: newEmail }),
});
</script>The combined controls — synchronizer token, double-submit cookie validation, custom header on JSON requests, SameSite=Lax on the session cookie, Origin check as the layered server-side guard — shrink CSRF to "the attacker has compromised the application's origin," which is the XSS class. At that point the CSRF defense relies on the XSS defense, and we are back to the layered story this guide opens with.
XSS vs SQL Injection — The Adjacent Comparison
The third comparison developers ask about is cross site scripting and sql injection — both labeled "injection," both on OWASP A03 since 2021, both involving untrusted data being interpreted as code. The difference is which side of the application the interpreter lives on. Cross site scripting vs sql injection reduces to a single distinction: SQL injection runs server-side, in the database engine, with whatever privileges the application's database connection holds; XSS runs client-side, in the victim's browser, with whatever privileges the application's origin grants to JavaScript. SQL injection's data context is rows in the application's database; XSS's data context is the victim's session, cookies, and DOM. The defenses are different — parameterized queries for SQLi, output encoding and CSP for XSS — and detection looks different too. The OWASP A03 absorption highlights that both share the underlying anatomy of "untrusted data flows into an interpreter without a separate channel for code and data," but the operational disciplines are distinct enough that most teams cover them in separate code-review checklists. The full server-side injection treatment lives in our OWASP A03 injection developer guide.
Closing — Defend Against XSS, CSRF, and SQLi Together
The questions developers ask — xss vs csrf, cross site request forgery vs cross site scripting, cross site scripting and sql injection — are usually motivated by a sense that one of the three is "the same as" another. The framing this guide pushes back on is that any of these classes is solved by defending against another. Output encoding does not stop CSRF. SameSite cookies do not stop XSS. Parameterized queries do not stop either. CSRF tokens read from the DOM by an XSS payload do not stop forgery. Each class has a distinct attack mechanism, a distinct defense layer, and a distinct detection pipeline. A complete posture applies all three.
The layering connects upward to the OWASP Top 10. CSRF is part of A01 Broken Access Control in the 2021 reorganization — the recognition that ambient cookie authentication, when state-changing requests can be forged from another origin, is a missing access-control check. XSS and SQL injection both live under A03 Injection. The defenses for all three share a common spine: validate input at the boundary, parameterize at the protocol level, encode for the destination context, enforce origin-based controls at every state change, and assume any one layer can fail. That spine — applied across rendering surfaces, query construction, and request handling together — is what separates applications that ship with one of these classes covered from applications that ship without any of the three.
XSS, CSRF, and SQLi Are Three Disciplines, Not One.
The teams that ship without XSS, CSRF, and SQL injection share the same property: developers who recognize each pattern in their stack's specific shape and reach for the right defense at the right layer. SecureCodingHub builds the context-aware encoding, the CSRF-token-and-SameSite fluency, and the parameterization-first discipline that turns these three classes from recurring scanner findings into something developers catch themselves at code-review time. If your team is closing one of the three but not all three, we'd be glad to show you how our program covers the full spine.
See the Platform