Learning Guides
Menu

Security Patterns

11 min readFrontend Patterns & Concepts

Security Patterns

Security isn't an afterthought—it's fundamental. Understanding common vulnerabilities and how to prevent them is essential for every frontend developer.

Cross-Site Scripting (XSS)

Attackers inject malicious scripts that execute in users' browsers.

Types of XSS

PLAINTEXT
Stored XSS: Malicious script stored in database
├── User posts comment: <script>stealCookies()</script>
├── Saved to database
└── Executed when others view the comment
 
Reflected XSS: Script in URL executed immediately
├── Attacker crafts URL: site.com/search?q=<script>evil()</script>
├── Victim clicks link
└── Script executes in victim's browser
 
DOM-based XSS: Client-side code processes unsafe data
├── page.html#<script>evil()</script>
├── JS reads location.hash and inserts into DOM
└── Script executes

Prevention

JAVASCRIPT
// ❌ DANGEROUS: Direct HTML insertion
element.innerHTML = userInput;
document.write(userInput);
 
// ✅ SAFE: Text content (automatically escaped)
element.textContent = userInput;
 
// ✅ SAFE: Using DOM APIs
const div = document.createElement("div");
div.textContent = userInput;
parent.appendChild(div);
 
// Sanitizing HTML when you must render it
import DOMPurify from "dompurify";
 
function SafeHTML({ html }) {
  const sanitized = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
    ALLOWED_ATTR: ["href", "target"],
  });
 
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}
 
// React automatically escapes by default
function SafeComponent({ userInput }) {
  return <div>{userInput}</div>; // Escaped automatically
}
 
// ❌ DANGEROUS: React escape hatch
function DangerousComponent({ userInput }) {
  return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
}

URL Handling

JAVASCRIPT
// ❌ DANGEROUS: Unvalidated URLs
<a href={userProvidedUrl}>Link</a>;
 
// This allows: javascript:alert('XSS')
 
// ✅ SAFE: Validate URL protocol
function isSafeUrl(url) {
  try {
    const parsed = new URL(url);
    return ["http:", "https:", "mailto:"].includes(parsed.protocol);
  } catch {
    return false;
  }
}
 
function SafeLink({ url, children }) {
  const href = isSafeUrl(url) ? url : "#";
  return <a href={href}>{children}</a>;
}

Content Security Policy (CSP)

Tells browsers what resources are allowed to load.

HTML
<!-- HTTP header or meta tag -->
<meta
  http-equiv="Content-Security-Policy"
  content="
  default-src 'self';
  script-src 'self' 'nonce-abc123';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
"
/>
 
<!-- Inline scripts need nonce -->
<script nonce="abc123">
  // Allowed because nonce matches
</script>
JAVASCRIPT
// Next.js CSP configuration
// next.config.js
const cspHeader = `
  default-src 'self';
  script-src 'self' 'unsafe-eval' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  img-src 'self' blob: data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
`;
 
module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Content-Security-Policy",
            value: cspHeader.replace(/\n/g, ""),
          },
        ],
      },
    ];
  },
};

Cross-Site Request Forgery (CSRF)

Attackers trick users into performing unwanted actions on sites where they're authenticated.

How CSRF Works

PLAINTEXT
1. User logs into bank.com (session cookie set)
2. User visits evil.com
3. evil.com has: <img src="bank.com/transfer?to=attacker&amount=1000">
4. Browser sends request WITH user's session cookie
5. Transfer executes as authenticated user

Prevention

JAVASCRIPT
// 1. CSRF Tokens
// Server generates unique token per session/request
 
// Server-side (example)
app.get("/form", (req, res) => {
  const csrfToken = generateToken();
  req.session.csrfToken = csrfToken;
  res.render("form", { csrfToken });
});
 
// Client-side
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value={csrfToken} />
  <input type="text" name="amount" />
  <button type="submit">Transfer</button>
</form>;
 
// Or with fetch
async function submitForm(data) {
  const response = await fetch("/api/transfer", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": getCsrfToken(),
    },
    body: JSON.stringify(data),
    credentials: "include",
  });
}
 
// 2. SameSite Cookies (modern approach)
// Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
 
// 3. Check Origin/Referer headers (server-side)
app.use((req, res, next) => {
  const origin = req.headers.origin;
  const referer = req.headers.referer;
 
  if (origin && !allowedOrigins.includes(origin)) {
    return res.status(403).json({ error: "Invalid origin" });
  }
 
  next();
});

Injection Attacks

Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query. While SQL injection primarily happens on the backend, frontend developers must understand it to build secure applications end-to-end.

Why frontend developers should care:

  • You control what data gets sent to the API
  • Client-side validation is the first line of defense (but not the only one)
  • Understanding attacks helps you design safer interfaces
  • Some injection attacks (like template injection) happen client-side

SQL Injection (via API)

The attack: An attacker enters malicious SQL in a form field. If the backend concatenates this directly into a query, the attacker can read, modify, or delete data.

Frontend's role: Validate input format before sending. But remember—never rely solely on frontend validation. Attackers can bypass your UI entirely.

JAVASCRIPT
// ❌ NEVER: String concatenation
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Attacker: email = "'; DROP TABLE users; --"
 
// ✅ ALWAYS: Parameterized queries (backend)
const query = "SELECT * FROM users WHERE email = $1";
db.query(query, [email]);
 
// Frontend: validate before sending
function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

Command Injection Prevention

The attack: If user input is passed to shell commands (like file uploads with user-provided names), attackers can inject shell commands.

Example: A filename like ; rm -rf / could be catastrophic if passed unsanitized to a shell command.

Frontend's role: Sanitize filenames and user-provided strings before sending to the server. Remove or escape special characters.

JAVASCRIPT
// Frontend validation
function sanitizeFilename(filename) {
  // Remove path traversal and special characters
  return filename
    .replace(/\.\./g, "")
    .replace(/[/\\]/g, "")
    .replace(/[^a-zA-Z0-9._-]/g, "");
}

Template Injection

The attack: If user input is embedded in template strings or passed to eval(), attackers can execute arbitrary JavaScript in other users' browsers.

Why eval is dangerous:

  • Executes ANY string as code
  • Has full access to your application's scope
  • Attacker can steal data, hijack sessions, modify the DOM

Rule: Never use eval(), new Function(), or innerHTML with user-provided content.

JAVASCRIPT
// ❌ DANGEROUS: eval and Function constructor
eval(userInput);
new Function(userInput)();
 
// ❌ DANGEROUS: Template literal injection
const template = `<div>${userInput}</div>`;
 
// ✅ SAFE: Use safe templating
function renderTemplate(template, data) {
  return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
    return escapeHtml(data[key] || "");
  });
}
 
function escapeHtml(str) {
  const div = document.createElement("div");
  div.textContent = str;
  return div.innerHTML;
}

Authentication Security

Authentication is the most security-critical part of most applications. Mistakes here can expose all user data.

Secure Token Storage

The problem: Where do you store authentication tokens on the client? Each option has trade-offs:

StorageXSS VulnerableCSRF VulnerablePersists
localStorageYESNoYes
sessionStorageYESNoNo
HttpOnly CookieNoYES (mitigatable)Yes
Memory (JS variable)NoNoNo

The recommended approach:

  • Store refresh tokens in HttpOnly cookies (XSS can't read them)
  • Store short-lived access tokens in memory (cleared on refresh)
  • Implement token refresh to get new access tokens

This way, XSS can't steal long-lived tokens, and CSRF is prevented by not auto-sending access tokens.

JAVASCRIPT
// ❌ DANGEROUS: localStorage for auth tokens
localStorage.setItem("authToken", token);
// Vulnerable to XSS - any script can read it
 
// ✅ BETTER: HttpOnly cookies (set by server)
// Set-Cookie: authToken=xxx; HttpOnly; Secure; SameSite=Strict
 
// ✅ ACCEPTABLE: Memory storage (for SPAs)
let authToken = null;
 
function setToken(token) {
  authToken = token;
  // Token cleared on page refresh
}
 
// ✅ For refresh tokens
// Store refresh token in HttpOnly cookie
// Store short-lived access token in memory
class TokenManager {
  constructor() {
    this.accessToken = null;
    this.refreshPromise = null;
  }
 
  async getToken() {
    if (this.accessToken && !this.isExpired(this.accessToken)) {
      return this.accessToken;
    }
 
    // Refresh token stored in HttpOnly cookie
    // Server validates cookie and returns new access token
    return this.refresh();
  }
 
  async refresh() {
    if (this.refreshPromise) return this.refreshPromise;
 
    this.refreshPromise = fetch("/api/refresh", {
      method: "POST",
      credentials: "include", // Send HttpOnly cookie
    })
      .then((res) => res.json())
      .then((data) => {
        this.accessToken = data.accessToken;
        return this.accessToken;
      })
      .finally(() => {
        this.refreshPromise = null;
      });
 
    return this.refreshPromise;
  }
}

Password Handling

Golden rules for passwords:

  1. Never store passwords client-side (no localStorage, no cookies)
  2. Never log passwords (not even during development)
  3. Never send in URLs (URLs are logged in server logs, browser history)
  4. Always use HTTPS (passwords sent over HTTP can be intercepted)
  5. Clear from memory after sending (minimize exposure window)

Additional best practices:

  • Use type="password" (hides input, enables password managers)
  • Add autoComplete="current-password" (helps password managers)
  • Consider adding password strength indicators
  • Never limit password length or character sets
JAVASCRIPT
// NEVER store passwords client-side
// NEVER log passwords
// NEVER send passwords in URLs
 
// Secure password form
function LoginForm() {
  const [password, setPassword] = useState("");
 
  const handleSubmit = async (e) => {
    e.preventDefault();
 
    // Send over HTTPS only
    await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ password }),
      credentials: "include",
    });
 
    // Clear password from memory
    setPassword("");
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        autoComplete="current-password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

Third-Party Dependencies

Your application's security is only as strong as its weakest dependency. A vulnerability in a popular npm package can affect millions of sites.

The supply chain problem:

  • Average project has hundreds of dependencies (including transitive)
  • Each dependency is code you didn't write and may not have audited
  • Attackers target popular packages (event-stream incident, ua-parser-js)
  • CDN-hosted libraries can be tampered with

Auditing Dependencies

Regular audits are essential. Run these commands regularly (ideally in CI/CD):

BASH
# Check for known vulnerabilities
npm audit
yarn audit
pnpm audit
 
# Fix automatically if possible
npm audit fix
 
# Check specific package
npx snyk test

Subresource Integrity (SRI)

The problem: When loading scripts from CDNs, you're trusting that CDN to serve the exact code you expect. If the CDN is compromised, attackers could inject malicious code into every site using it.

The solution: SRI lets you specify a cryptographic hash of the expected file. The browser will only execute the script if its hash matches.

How it works:

  1. You calculate a hash of the library file (SHA-256, SHA-384, or SHA-512)
  2. You add the hash to the integrity attribute
  3. Browser downloads the file and calculates its hash
  4. If hashes don't match, browser refuses to execute the script

Generate SRI hashes: Use shasum command or https://www.srihash.org/

HTML
<!-- Verify CDN resources haven't been tampered with -->
<script
  src="https://cdn.example.com/library.js"
  integrity="sha384-abc123..."
  crossorigin="anonymous"
></script>
 
<link
  rel="stylesheet"
  href="https://cdn.example.com/styles.css"
  integrity="sha384-xyz789..."
  crossorigin="anonymous"
/>

Sandboxing Iframes

HTML
<!-- Restrict iframe capabilities -->
<iframe
  src="https://third-party.com/widget"
  sandbox="allow-scripts allow-same-origin"
  allow="camera 'none'; microphone 'none'"
></iframe>
 
<!-- Sandbox options:
  allow-scripts: Allow JavaScript
  allow-same-origin: Allow same-origin access
  allow-forms: Allow form submission
  allow-popups: Allow window.open
  allow-modals: Allow alerts/confirms
-->

Secure Communication

HTTPS Only

JAVASCRIPT
// Redirect HTTP to HTTPS (server-side)
app.use((req, res, next) => {
  if (!req.secure && process.env.NODE_ENV === "production") {
    return res.redirect(301, `https://${req.headers.host}${req.url}`);
  }
  next();
});
 
// HTTP Strict Transport Security (HSTS)
// Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Secure Headers

JAVASCRIPT
// Essential security headers (Next.js example)
module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "X-Content-Type-Options",
            value: "nosniff",
          },
          {
            key: "X-Frame-Options",
            value: "DENY",
          },
          {
            key: "X-XSS-Protection",
            value: "1; mode=block",
          },
          {
            key: "Referrer-Policy",
            value: "strict-origin-when-cross-origin",
          },
          {
            key: "Permissions-Policy",
            value: "camera=(), microphone=(), geolocation=()",
          },
        ],
      },
    ];
  },
};

Input Validation

JAVASCRIPT
// Comprehensive input validation
const validators = {
  email: (value) => {
    const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return pattern.test(value);
  },
 
  username: (value) => {
    // Alphanumeric, 3-20 chars
    const pattern = /^[a-zA-Z0-9_]{3,20}$/;
    return pattern.test(value);
  },
 
  url: (value) => {
    try {
      const url = new URL(value);
      return ["http:", "https:"].includes(url.protocol);
    } catch {
      return false;
    }
  },
 
  phone: (value) => {
    // Basic phone validation
    const pattern = /^\+?[\d\s-]{10,}$/;
    return pattern.test(value);
  },
 
  safeString: (value, maxLength = 1000) => {
    if (typeof value !== "string") return false;
    if (value.length > maxLength) return false;
    // No control characters except newlines
    const pattern = /^[\x20-\x7E\n\r\t]*$/;
    return pattern.test(value);
  },
};
 
function validateInput(value, type) {
  const validator = validators[type];
  if (!validator) throw new Error(`Unknown validator: ${type}`);
  return validator(value);
}

Security Checklist

PLAINTEXT
Authentication & Authorization:
□ Use HttpOnly cookies for session tokens
□ Implement proper logout (invalidate server-side)
□ Use CSRF protection for state-changing requests
□ Validate authorization on every protected request
 
Data Handling:
□ Never trust client-side data
□ Validate and sanitize all inputs
□ Encode output appropriately (HTML, URL, JS)
□ Use parameterized queries
 
Transport:
□ Use HTTPS everywhere
□ Set secure cookie flags
□ Implement HSTS
□ Use SRI for third-party resources
 
Headers:
□ Set Content-Security-Policy
□ Set X-Content-Type-Options
□ Set X-Frame-Options
□ Set Referrer-Policy
 
Dependencies:
□ Audit regularly (npm audit)
□ Keep dependencies updated
□ Use lockfiles
□ Review new dependencies before adding

Summary

VulnerabilityPrevention
XSSEscape output, CSP, sanitize HTML
CSRFCSRF tokens, SameSite cookies
InjectionValidate input, parameterized queries
Token theftHttpOnly cookies, short-lived tokens
ClickjackingX-Frame-Options, CSP frame-ancestors
MITMHTTPS, HSTS, secure cookies

Key Principles:

  1. Never trust user input
  2. Defense in depth (multiple layers)
  3. Fail securely (deny by default)
  4. Keep dependencies updated
  5. Audit and test regularly