Frontend security is often overlooked, with many developers assuming that security concerns are primarily a backend responsibility. However, frontend code is directly exposed to users and can be a significant attack vector. Implementing proper security measures in your frontend code is essential for protecting:
User data and privacy
Application functionality
Backend systems
Your organization’s reputation
Security is a shared responsibility across the entire application stack. Even the most secure backend can be compromised if the frontend contains vulnerabilities.
Attackers inject malicious scripts that execute in users’ browsers.Impact: Can steal cookies, session tokens, and sensitive data, or perform actions on behalf of the user.
Cross-Site Request Forgery (CSRF)
Forces authenticated users to perform unwanted actions on a web application.Impact: Can lead to unauthorized transactions, data changes, or account compromise.
Clickjacking
Tricks users into clicking on something different from what they perceive.Impact: Can lead to unwanted actions, downloads, or data exposure.
Sensitive Data Exposure
Exposing sensitive data in frontend code or browser storage.Impact: Can lead to data breaches, identity theft, or account takeover.
Insecure Dependencies
Using libraries or frameworks with known vulnerabilities.Impact: Can introduce various security issues depending on the vulnerability.
Client-Side Logic Vulnerabilities
Relying solely on client-side validation or exposing sensitive business logic.Impact: Can lead to data manipulation, bypassing restrictions, or business logic abuse.
Be careful with DOM manipulation methods that can execute JavaScript.
Copy
// Dangerous methods that can lead to XSS:document.write(userInput);element.innerHTML = userInput;element.outerHTML = userInput;element.insertAdjacentHTML('beforebegin', userInput);element.insertAdjacentHTML('afterbegin', userInput);element.insertAdjacentHTML('beforeend', userInput);element.insertAdjacentHTML('afterend', userInput);// Safer alternatives:element.textContent = userInput; // For textelement.setAttribute('value', userInput); // For attributes (except event handlers)
When you need to allow some HTML, use a sanitization library.
Copy
// Using DOMPurifyimport DOMPurify from 'dompurify';const userHtml = getUserGeneratedHtml();const sanitizedHtml = DOMPurify.sanitize(userHtml);// Now safe to insertelement.innerHTML = sanitizedHtml;
Implement CSRF tokens for state-changing operations.
Copy
<!-- Form with CSRF token --><form action="/api/update-profile" method="POST"> <input type="hidden" name="csrf_token" value="random-token-value"> <!-- Other form fields --> <button type="submit">Update Profile</button></form>
// Advanced frame-busting(function() { // If we're in a frame if (window !== window.top) { // Try to break out window.top.location = window.location; // If we're still in a frame after attempting to break out // (can happen if the parent page uses security measures) setTimeout(function() { if (window !== window.top) { // Make the UI unusable document.body.innerHTML = 'This site cannot be displayed in a frame.'; document.body.style.backgroundColor = '#FFF'; document.body.style.color = '#000'; document.body.style.padding = '20px'; document.body.style.position = 'fixed'; document.body.style.top = '0'; document.body.style.left = '0'; document.body.style.right = '0'; document.body.style.bottom = '0'; document.body.style.zIndex = '9999999'; } }, 1); }})();
// BAD: Hardcoded API keyconst apiKey = 'sk_live_abcdef123456';// GOOD: Get API key from backendasync function fetchDataWithAuth() { // Get a short-lived token from your backend const { token } = await fetch('/api/get-auth-token').then(r => r.json()); // Use the token return fetch('/api/data', { headers: { 'Authorization': `Bearer ${token}` } });}
Browser storage mechanisms (localStorage, sessionStorage) are not secure for sensitive data.
Copy
// BAD: Storing sensitive data in localStoragelocalStorage.setItem('creditCard', '1234-5678-9012-3456');// BETTER: Store only non-sensitive datalocalStorage.setItem('userPreferences', JSON.stringify({ theme: 'dark', fontSize: 'large' }));// For sensitive data that must be persisted client-side, consider encrypting it// (though this has limitations and should be used carefully)import CryptoJS from 'crypto-js';function secureStore(key, data, secretKey) { const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), secretKey).toString(); sessionStorage.setItem(key, encrypted);}function secureRetrieve(key, secretKey) { const encrypted = sessionStorage.getItem(key); if (!encrypted) return null; const decrypted = CryptoJS.AES.decrypt(encrypted, secretKey).toString(CryptoJS.enc.Utf8); return JSON.parse(decrypted);}
// Instead of importing a large library for a simple function// BADimport _ from 'lodash';const reversed = _.reverse([1, 2, 3]);// GOOD: Use native methods when possibleconst reversed = [1, 2, 3].reverse();// If you need specific functions, import only what you need// BETTER than importing the whole libraryimport reverse from 'lodash/reverse';const reversed = reverse([1, 2, 3]);
// Store tokens securelyfunction storeAuthToken(token) { // For SPA, store in memory when possible // This token will be lost on page refresh, but that's often acceptable // for better security const authState = { token, expiresAt: calculateExpiry(token) }; // Store in a closure instead of global variable return { getToken: () => { // Check if token is expired if (Date.now() >= authState.expiresAt) { return null; } return authState.token; }, clearToken: () => { authState.token = null; authState.expiresAt = 0; } };}// Usageconst auth = storeAuthToken(receivedToken);// When making authenticated requestsfetch('/api/data', { headers: { 'Authorization': `Bearer ${auth.getToken()}` }});// On logoutauth.clearToken();
Prevent sensitive data in JSON responses from being stolen via script tags.
Copy
// Server-side: Prefix JSON responses with a character that makes it invalid JavaScriptapp.get('/api/user-data', (req, res) => { const userData = { /* sensitive user data */ }; res.send(`)]}',\n${JSON.stringify(userData)}`);});// Client-side: Strip the prefix before parsingfetch('/api/user-data') .then(response => response.text()) .then(text => { // Remove the anti-hijacking prefix const json = text.replace(/^\)\]\}',\n/, ''); return JSON.parse(json); }) .then(data => { // Use the data safely });
Be careful with DOM APIs that can lead to security issues.
Copy
// Dangerous: Using location.href with user inputconst userInput = document.getElementById('search').value;window.location.href = '/search?q=' + userInput; // Vulnerable to JavaScript injection// Safe: Encode parameters properlyconst userInput = document.getElementById('search').value;window.location.href = '/search?q=' + encodeURIComponent(userInput);// Dangerous: Using eval or new Functionconst userInput = document.getElementById('expression').value;const result = eval(userInput); // Never do this!// Safe: Use alternatives to eval// For JSON parsing:const jsonString = getJsonString();const data = JSON.parse(jsonString);// For dynamic math calculations:const expression = document.getElementById('expression').value;// Use a math expression parser library instead of eval