Client-Side Vulnerabilities
Browser-trust vulnerabilities — XSS types, contextual output encoding, CSP, CSRF, CORS, clickjacking, tabnabbing, prototype pollution, postMessage and WebSocket security.
16 questions
JuniorTheoryVery commonWhat is cross-site request forgery (CSRF), and when is a money endpoint vulnerable?
What is cross-site request forgery (CSRF), and when is a money endpoint vulnerable?
CSRF makes a victim's browser send a state-changing request to your site from an attacker's page; cookies ride along automatically, so the server sees a legitimate user. The endpoint is vulnerable when it authenticates by cookie alone and needs no unguessable value.
Common mistakes
- ✗Confusing CSRF with XSS — the forged request's response is normally unreadable
- ✗Believing HTTPS or an
HttpOnlycookie prevents CSRF - ✗Assuming state-changing
GEThandlers cannot be forged
Follow-up questions
- →Why can the attacker's page usually not read the forged response body?
- →How does the
SameSitecookie attribute change the default exposure?
JuniorTheoryVery commonWhat are the three types of cross-site scripting (XSS), and what does injected script gain?
What are the three types of cross-site scripting (XSS), and what does injected script gain?
Reflected XSS echoes attacker input back in the response; stored XSS persists it and serves it to every viewer; DOM XSS never reaches the server — client script writes untrusted data into a sink. All three run in your origin and act as the user.
Common mistakes
- ✗Thinking an
HttpOnlycookie prevents XSS rather than blunting token theft - ✗Expecting DOM XSS to show up in server-rendered output or access logs
- ✗Treating reflected XSS as harmless because it needs a crafted link
Follow-up questions
- →Why is contextual output encoding the fix rather than blacklisting input characters?
- →How does a Content Security Policy (CSP) limit what already-injected script can do?
MiddleTheoryVery commonWhy is echoing back the Origin header with credentials a Cross-Origin Resource Sharing flaw?
Why is echoing back the Origin header with credentials a Cross-Origin Resource Sharing flaw?
Echoing the request Origin into Access-Control-Allow-Origin together with credentials lets any site read authenticated responses from your API. null is not a safe sentinel — sandboxed iframes send it — so allowlist exact origins instead of reflecting.
Common mistakes
- ✗Thinking the same-origin policy still hides the body after a permissive header
- ✗Allowlisting
nullas if only local files sent it - ✗Validating
Originwith a substring or suffix match
Follow-up questions
- →Which browsing contexts send an
Originofnull? - →Why does the browser reject a wildcard when credentials are requested?
JuniorTheoryCommonCompare SameSite cookies, synchronizer tokens and double-submit as CSRF defenses
Compare SameSite cookies, synchronizer tokens and double-submit as CSRF defenses
SameSite=Lax or Strict stops cookies riding cross-site requests — cheap, but a browser default, not a per-request check. A synchronizer token is server-stored and verified per request, the strongest option. Double-submit is stateless but subdomain control weakens it.
Common mistakes
- ✗Thinking the
HttpOnlyorSecurecookie flags defend against CSRF - ✗Relying on
SameSitealone and skipping any per-request verification - ✗Missing that double-submit collapses under attacker subdomain control
Follow-up questions
- →Why does a compromised sibling subdomain undermine the double-submit pattern?
- →When does
SameSite=Laxstill permit a cross-site state change?
JuniorTheoryCommonHow do you trace a DOM-based XSS from its untrusted source to its dangerous sink?
How do you trace a DOM-based XSS from its untrusted source to its dangerous sink?
Start at the sources — location.hash, location.search, document.referrer, postMessage data — and follow the value to sinks that parse or execute it, such as innerHTML, document.write and eval. Any unsanitised source-to-sink path is the bug.
Common mistakes
- ✗Expecting a DOM XSS payload to appear in server access logs
- ✗Trusting values from
localStorageor the URL fragment as user-owned - ✗Confusing a text sink like
textContentwith a parsing sink likeinnerHTML
Follow-up questions
- →Why does the URL fragment never reach the server, and what follows from that?
- →How do Trusted Types make a dangerous sink refuse a raw string?
JuniorTheoryCommonWhy must output encoding match the sink's context, and why is one encoder not enough?
Why must output encoding match the sink's context, and why is one encoder not enough?
Each sink parses differently — HTML text needs entity encoding, attributes need quoting, JavaScript strings need JS escaping, URLs need percent-encoding. HTML-encoded data landing in a <script> block or an href stays executable, so pick the encoder by destination.
Common mistakes
- ✗Encoding once at input instead of at each output sink
- ✗Assuming database-sourced values are trusted and need no encoding
- ✗Treating character blacklisting or a WAF as a substitute for encoding
Follow-up questions
- →Which sinks can no encoder make safe, and what do you do for those instead?
- →How does a template engine with auto-escaping decide the context for you?
MiddleTheoryCommonWhy is cross-origin sharing (CORS) not an authorization control, and what does a preflight do?
Why is cross-origin sharing (CORS) not an authorization control, and what does a preflight do?
The same-origin policy protects you by default; CORS only relaxes it, so a permissive config is the vulnerability, not a defense. It governs what a browser lets script read — non-browser clients ignore it. The preflight checks method and headers, never authorisation.
Common mistakes
- ✗Treating CORS as an access control rather than a relaxation of the same-origin policy
- ✗Assuming non-browser clients such as
curlare constrained by CORS - ✗Thinking the preflight authenticates or authorises the caller
Follow-up questions
- →Which request shapes skip the preflight entirely, and why does that matter?
- →What server-side check must still run no matter what the CORS policy says?
MiddleTheoryCommonHow do Content Security Policy (CSP) nonces and hashes work, and why does unsafe-inline gut it?
How do Content Security Policy (CSP) nonces and hashes work, and why does unsafe-inline gut it?
The browser runs only scripts the policy allows. A per-response random nonce on your own <script> tags, or a sha256 hash pinning exact content, marks them — injected script carries neither. unsafe-inline allows every inline script, including the injected one.
Common mistakes
- ✗Reusing one nonce across responses instead of regenerating it per response
- ✗Thinking CSP inspects script contents for dangerous patterns
- ✗Believing
unsafe-inlineaffects only inline event handlers
Follow-up questions
- →Why is
strict-dynamicused alongside a nonce on script-heavy pages? - →What does a policy still not protect against once your own script executes?
MiddleTheoryCommonWhat must a postMessage receiver validate, and why is event.data still untrusted afterwards?
What must a postMessage receiver validate, and why is event.data still untrusted afterwards?
Any window can post to yours, so the handler must compare event.origin against an exact expected origin before acting — never a substring test — and the sender should target a specific origin, not *. Even then event.data is data, so render it with textContent.
Common mistakes
- ✗Using a substring or suffix test on
event.origininstead of an exact comparison - ✗Believing
postMessageis already restricted to same-origin windows - ✗Treating a passing origin check as making
event.datasafe for an HTML sink
Follow-up questions
- →Why should the sender pass a specific target origin instead of
*? - →How do Trusted Types stop
event.datafrom reaching a parsing sink?
JuniorTheoryOccasionalWhat is clickjacking, and how do you control which sites are allowed to frame your page?
What is clickjacking, and how do you control which sites are allowed to frame your page?
Clickjacking loads your page in a transparent frame over attacker content, so the victim's click lands on your real button while they believe they clicked elsewhere. Control framing with the frame-ancestors directive, keeping X-Frame-Options for old browsers.
Common mistakes
- ✗Believing the same-origin policy stops your page being framed by another site
- ✗Relying on a script frame-buster instead of a response header
- ✗Confusing clickjacking with an injection flaw fixed by output encoding
Follow-up questions
- →Why does
frame-ancestorssupersede the legacyX-Frame-Optionsheader? - →Which UI actions become far riskier when they have no confirmation step?
JuniorTheoryOccasionalWhy do user-submitted outbound links opened with target=_blank need rel=noopener?
Why do user-submitted outbound links opened with target=_blank need rel=noopener?
Without it the opened page gets a window.opener handle to your tab and can navigate it to a phishing copy of your login page — reverse tabnabbing. rel=noopener severs that handle; rel=noreferrer also drops the Referer. Set it explicitly on rendered links.
Common mistakes
- ✗Thinking
window.openerlets the opened page read your DOM rather than navigate it - ✗Assuming a new tab holds no reference back to its opener
- ✗Treating
noreferreras purely an analytics or privacy setting
Follow-up questions
- →Where does the modern implicit
noopenerdefault not apply? - →How does reverse tabnabbing differ from the web-attack class
CSRF?
MiddleDebuggingOccasionalA Content Security Policy report shows a blocked inline script — diagnose the client-side flaw
A Content Security Policy report shows a blocked inline script — diagnose the client-side flaw
An inline script without a nonce reached the DOM although this route ships none, so it was injected client-side — DOM XSS. The search term flows from the URL into the results header through a parsing sink. CSP blocked and reported it; fix the sink, not the policy.
Open full question →Common mistakes
- ✗Dismissing violation reports as extension noise without checking the route
- ✗Silencing the report with
unsafe-inlineinstead of fixing the sink - ✗Assuming an inline violation implies server-rendered markup
Follow-up questions
- →Which source in this flow do you trace first, and to which sink?
- →Why does an empty
script-samplefield not rule out an injected payload?
MiddleCodeOccasionalFix the insecure postMessage handler so it validates the sender and renders safely
Fix the insecure postMessage handler so it validates the sender and renders safely
Two defects. The handler never checks event.origin, so any window can drive it, and it writes user text into innerHTML, a parsing sink. Compare event.origin to https://app.example.com exactly and return on mismatch, then render with textContent.
Common mistakes
- ✗Checking
event.sourceor an origin suffix instead of an exact origin match - ✗Escaping quote characters instead of switching off the parsing sink
- ✗Assuming a structured message channel makes the payload safe for
innerHTML
Follow-up questions
- →Why does returning early on an origin mismatch matter more than sanitising the value?
- →When would
textContentbe insufficient and Trusted Types be the better control?
MiddleTheoryOccasionalWhat is prototype pollution, and how does a polluted key escalate into a real vulnerability?
What is prototype pollution, and how does a polluted key escalate into a real vulnerability?
A recursive merge that copies attacker keys can write through __proto__, adding a property every plain object then inherits. Alone it is inert — it becomes a bug when a gadget reads that property as configuration, such as a template option or an HTML sink.
Common mistakes
- ✗Treating the prototype write as instant code execution rather than needing a gadget
- ✗Blocking only the literal
__proto__key and missingconstructor.prototype - ✗Assuming inherited properties never reach a template or DOM sink
Follow-up questions
- →Why does a
Mapor a null-prototype object stop the write from mattering? - →How would you hunt for a gadget in a dependency after confirming the write?
MiddleTheoryOccasionalWhy is a cookie-authenticated WebSocket open to cross-site hijacking, and what fixes it?
Why is a cookie-authenticated WebSocket open to cross-site hijacking, and what fixes it?
The handshake is an HTTP upgrade that carries cookies but is not governed by CORS, so an attacker's page can open a socket as the logged-in user and read the stream. Validate Origin server-side against an allowlist and authenticate with a short-lived token.
Common mistakes
- ✗Assuming CORS or the same-origin policy constrains a WebSocket handshake
- ✗Believing cookies are not attached to the upgrade request
- ✗Treating
wsstransport encryption as the answer to hijacking
Follow-up questions
- →Why is server-side
Originvalidation meaningful only for browser clients? - →How does a short-lived connection ticket differ from reusing the session cookie?
SeniorTheoryOccasionalHow do you roll out a strict Content Security Policy on a legacy app without breakage?
How do you roll out a strict Content Security Policy on a legacy app without breakage?
Ship Content-Security-Policy-Report-Only first with a report endpoint and let real traffic enumerate every inline handler and third-party host. Then strip inline onclick and eval, move code to external files, add per-response nonces, and enforce once reports flatline.
Common mistakes
- ✗Enforcing before report-only traffic has enumerated the real inline usage
- ✗Keeping
unsafe-inlinepermanently instead of removing inline handlers - ✗Believing a third-party host allowlist covers inline event handlers
Follow-up questions
- →Why does
strict-dynamicmake a third-party host allowlist largely redundant? - →How do you keep Subresource Integrity (SRI) hashes in step with third-party bundles?