Cross Site Request Forgery (CSRF, also called XSRF or Session Riding) is a web security attack that forces a logged in user to perform an unwanted action on a web application where they are currently authenticated. The attacker crafts a malicious link, image or form on a site they control. When the victim visits this page while still logged in to the target site (for example, an online banking portal, a WordPress admin or a webmail), the browser automatically sends the request together with the valid session cookie. The target server cannot distinguish the forged request from a real one and executes it: transferring money, changing the email address, deleting a post, granting admin rights. CSRF was number 5 in the OWASP Top 10 in 2007 and 2010, dropped out of the dedicated Top 10 in 2017 because major frameworks added built in defenses, and remains a category under A01 Broken Access Control in the OWASP Top 10 2021. The Samy worm on MySpace (October 2005, over 1 million infected profiles in 20 hours) is the most famous CSRF based incident in history.
How does a CSRF attack work?
The attacker needs three conditions:
- The victim is logged in to the target site (cookie based session is active in the browser).
- The target site executes state changing actions based on cookies alone, without any extra anti CSRF token.
- The victim visits an attacker controlled page or opens an attacker controlled email.
A simple example: the target bank uses https://bank.example.com/transfer?to=ACCOUNT&amount=1000 as a GET request. The attacker embeds in their page:
<img src="https://bank.example.com/transfer?to=ATTACKER&amount=10000" width="0" height="0">The browser loads the image, sends the request with the session cookie of the victim, and the bank executes the transfer. POST based versions use a hidden form auto submitted with JavaScript:
<form action="https://bank.example.com/transfer" method="POST" id="f">
<input name="to" value="ATTACKER">
<input name="amount" value="10000">
</form>
<script>document.getElementById('f').submit();</script>What are typical CSRF targets?
- Banking and payment: money transfers, adding a beneficiary, changing PIN.
- Webmail: changing the recovery email, adding a forwarding rule.
- Admin panels: creating a new admin user, changing the site URL, installing a plugin.
- Social networks: posting a status, sending friend requests (Samy worm pattern).
- E commerce: changing the shipping address right before checkout.
- Routers and IoT: many home routers expose admin interfaces on the local network with no CSRF protection. An attacker can change DNS settings via a CSRF on a malicious site.
How does CSRF differ from XSS?
| Property | CSRF | XSS |
|---|---|---|
| What it does | Forces the victim browser to send a request | Runs attacker JavaScript inside the victim page |
| Needs code execution on the target | No | Yes |
| Reads response from the target | No (Same Origin Policy blocks it) | Yes (it runs in the same origin) |
| Bypasses CSRF tokens | No, that is the whole point | Yes, XSS can read the token from the page |
| Main defense | CSRF tokens, SameSite cookies | Output encoding, Content Security Policy |
XSS is strictly more powerful than CSRF. If a site is vulnerable to XSS, CSRF defenses no longer help, because the attacker JavaScript can read the CSRF token from the DOM. Therefore preventing XSS is a prerequisite for any CSRF defense.
What are the standard defenses against CSRF?
- Anti CSRF token (synchronizer token pattern): the server includes a random, unpredictable token in every HTML form or AJAX request. The browser sends it back on submit and the server verifies it. The attacker page cannot read the token because of the Same Origin Policy. This is the OWASP recommended primary defense.
- SameSite cookie attribute: a cookie sent only with same site requests is immune to most CSRF. Three values:
Strict(never sent cross site),Lax(sent on top level navigation, default in Chrome since February 2020),None(sent always, must be combined with Secure). Setting session cookies toSameSite=LaxorStrictstops the vast majority of CSRF. - Double submit cookie: the server sets a random value in a cookie and the client sends it back in a custom header. The server checks they match. Works without server side session state.
- Origin and Referer header check: the server compares the
OriginorRefererheader against the expected domain. Useful as a defense in depth layer. - Custom request header for AJAX: requiring a header like
X-Requested-With: XMLHttpRequestblocks simple cross origin attacks because browsers force a CORS preflight when a custom header is added. - Re authentication for sensitive actions: ask for the password again before changing email, deleting account or transferring money. This is a usability vs security trade off.
- Use POST instead of GET for state changes: GET requests should never modify state. Image and link tags can trigger GET, but not POST, so this raises the bar.
What is SameSite=Lax and why is it the default now?
Chrome started enforcing SameSite=Lax by default for cookies without an explicit attribute in February 2020 (Chrome 80), Firefox followed in August 2020 (Firefox 79), Safari already enforced an even stricter ITP based policy since 2018. Lax means the cookie is sent on top level navigation (clicking a link) but not on cross site sub requests (images, iframes, XHR, form submissions to other origins). This single browser change neutralized most classic CSRF attacks on the open web. New cookies should still set SameSite explicitly because the default value is enforced inconsistently across browsers and older clients.
How does WordPress protect against CSRF?
WordPress uses Nonces (Number used Once, despite the name they are not strictly one time use). A Nonce is a short token generated from the user session, the current time and an action name. Core functions:
// Generate a Nonce for a form
<?php wp_nonce_field('delete_post_42', '_wpnonce'); ?>
// Or as a URL parameter
$url = wp_nonce_url(admin_url('options.php?action=delete&id=42'), 'delete_post_42');
// Or as a JavaScript constant
wp_localize_script('my-script', 'myAjax', array(
'nonce' => wp_create_nonce('my_ajax_action')
));
// Verify in the handler
check_admin_referer('delete_post_42');
// or for AJAX
check_ajax_referer('my_ajax_action', 'nonce');Nonces in WordPress have a default lifetime of 24 hours (split in two 12 hour windows so a Nonce close to expiry still validates for one tick). They are not session unique, two users with the same session age get different Nonces because the user ID is hashed in. Plugins that build custom admin actions must use Nonces, otherwise they expose CSRF holes that bug bounty hunters and Wordfence Threat Intelligence regularly publish in CVE feeds.
What about modern Single Page Apps and REST APIs?
SPAs that use cookies for authentication still need CSRF protection because cookies are sent automatically. Common patterns:
- Use
SameSite=Stricton the session cookie. - Require a custom header like
X-CSRF-Tokenon every state changing API call. The server returns the token via a GET endpoint after login and the frontend stores it in memory. - Or switch authentication to
Authorization: Bearer <JWT>header. Tokens stored in JavaScript memory are not automatically attached by the browser, so CSRF is no longer applicable. This shifts the risk to XSS, so a strict CSP becomes essential.
Real world CSRF incidents
- Samy worm on MySpace (October 2005): a CSRF + XSS combo that made every viewer of an infected profile also infected. Over 1 million profiles in 20 hours. Samy Kamkar was sentenced to community service.
- YouTube (2008): a CSRF allowed adding any video to a user playlist or sending friend invites.
- Netflix (2006): CSRF allowed adding movies to a queue or changing shipping address.
- Numerous WordPress plugins: hundreds of CVEs every year for missing Nonces in admin AJAX endpoints. Examples in 2024 include Forminator, LiteSpeed Cache and Royal Elementor Addons.
- Home routers: a 2018 study found CSRF in over 70 percent of consumer routers from Asus, Netgear, D-Link allowing DNS hijack.
How does InspectWP help with CSRF?
InspectWP analyzes the response headers of every scanned URL and reports on Set-Cookie attributes including SameSite and Secure. Cookies without a SameSite attribute or with SameSite=None on session cookies are flagged in the security section.