Frontend Security: Protecting Against Attacks
Frontend security is critical but often overlooked. XSS, CSRF, and other vulnerabilities can steal user data or compromise applications.
Cross-Site Scripting (XSS)
XSS is injecting malicious JavaScript into web pages.
Stored XSS: Attacker stores malicious code in database (via form input). When users load the page, code executes in their browser.
User submits comment: <script>alert('hacked')</script>
System stores it as-is.
Next user loads page → script executes in their browser.
Reflected XSS: Attacker tricks user into clicking malicious link.
Attacker sends: https://example.com/search?q=<script>stealCookie()</script>
User clicks link.
Server reflects query parameter in response → script executes.
Defense: Input Sanitization
Never trust user input. Sanitize everything:
// Bad: Raw HTML
const comment = userInput // <script>alert('xss')</script>
document.getElementById('comments').innerHTML = comment
// Good: Text-only
const comment = userInput
document.getElementById('comments').textContent = comment
// Good: Sanitized HTML (if HTML is needed)
import DOMPurify from 'dompurify'
const cleanComment = DOMPurify.sanitize(userInput, { ALLOWED_TAGS: ['b', 'i', 'u'] })
document.getElementById('comments').innerHTML = cleanComment
Defense: Context-Aware Encoding
Different contexts require different encoding:
// HTML context: Encode <, >, &
const text = userInput.replace(/[<>&]/g, (c) => ({
'<': '<',
'>': '>',
'&': '&',
}[c]))
// JavaScript context: Escape quotes and backslashes
const jsString = userInput.replace(/[\'"]/g, '\$&')
// URL context: Encode special characters
const url = encodeURIComponent(userInput)
// CSS context: Avoid user input in CSS (use classes instead)
// Bad: element.style.backgroundColor = userColor
// Good: element.className = userColor // Map to predefined CSS classes
Defense: Content Security Policy (CSP)
CSP restricts where scripts can load from:
<!-- Only allow scripts from same origin; block inline scripts -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
<!-- Allow specific trusted domains -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://cdn.example.com" />
<!-- Strict: disallow inline scripts entirely -->
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; default-src 'none'" />
Server can also set CSP headers:
Content-Security-Policy: script-src 'self' https://trusted-cdn.com; style-src 'self' https://fonts.googleapis.com; default-src 'none'
Cross-Site Request Forgery (CSRF)
CSRF tricks users into making unwanted requests on behalf of attackers.
User logged into bank.com.
Attacker tricks user into visiting malicious.com.
malicious.com has: <img src="bank.com/transfer?to=attacker&amount=1000" />
Browser makes request to bank.com with user's cookies.
Transfer succeeds without user consent.
Defense: CSRF Tokens
Server generates unique token for each user session. Requests must include the token.
// Server generates token
const token = generateToken() // Unique per session
res.cookie('csrf-token', token)
res.send(`<form><input type="hidden" name="csrf-token" value="${token}" /></form>`)
// Client includes token
const token = document.querySelector('input[name="csrf-token"]').value
fetch('/api/transfer', {
method: 'POST',
headers: { 'X-CSRF-Token': token },
body: JSON.stringify({ to: 'recipient', amount: 1000 }),
})
// Server validates token
if (req.headers['x-csrf-token'] !== req.session.csrfToken) {
res.status(403).send('CSRF token invalid')
}
Defense: SameSite Cookies
Set SameSite=Strict on cookies. Cookies sent only to same site, not cross-site requests.
Set-Cookie: sessionId=abc123; SameSite=Strict
Clickjacking
Attacker overlays invisible iframe on top of legitimate site, tricks users into clicking.
<!-- Attacker's page -->
<iframe src="bank.com" style="opacity: 0; position: absolute; top: 0; left: 0;"></iframe>
<!-- Visible button that aligns with hidden iframe's "Transfer" button -->
<button style="position: absolute; top: 100px; left: 100px;">Claim Prize!</button>
Defense: X-Frame-Options Header
X-Frame-Options: DENY # Prevent framing entirely
X-Frame-Options: SAMEORIGIN # Allow only same-origin framing
X-Frame-Options: ALLOW-FROM https://trusted.com # Allow specific origins
Dependency Vulnerabilities
Third-party libraries have vulnerabilities. Keep them updated:
npm audit # Identify vulnerabilities
npm audit fix # Auto-fix if possible
npm outdated # Show out-of-date packages
Use lock files (package-lock.json) to ensure consistent versions across team.
Best Practices Checklist
- Sanitize all user input
- Never use
innerHTMLwith user data - Use
textContentor DOMPurify for HTML - Set CSP headers
- Use CSRF tokens on state-changing requests
- Set SameSite cookies
- Use X-Frame-Options header
- Keep dependencies updated
- Audit regularly with OWASP guidelines
- Use HTTPS everywhere
- Implement subresource integrity (SRI) for CDN scripts
Real Example
A comment system vulnerable to XSS:
Attack:
User submits: <img src=x onerror="stealCookie()">
System stores and displays to others.
Fix:
const comment = DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [] })
// Converts to: <img src=x onerror="stealCookie()">
// Displays as text, doesn't execute.
Result: Safe, user intent preserved, attack prevented.
Frequently asked questions
Should I use innerHTML or textContent?
textContent for user-generated content (safe, no parsing). innerHTML only for trusted content or sanitized input via DOMPurify.
What's the difference between SameSite=Strict and SameSite=Lax?
Strict: Never send cookies cross-site. Lax: Send on same-site requests and top-level navigation. Strict is more secure.
Do I need CSP if I already sanitize input?
Yes. CSP is defense-in-depth. Even with sanitization, CSP blocks inline scripts and restricts trusted sources.