ExpertArchitecture
Draw the trust boundaries for a typical SPA. Where must validation and authorisation live?
Everything that runs in the browser is untrusted, including your own code — a user controls it completely. So the boundaries are: browser to your API (every request is attacker-controllable, regardless of what the UI allows); your API to third-party services; and the browser to third-party scripts you embed, which run with your origin’s full privileges. The consequences: authorisation is decided server-side per request against the authenticated session, never from an id in the payload and never from a UI that hid the button. Input validation happens server-side for safety and client-side only for user experience. Client-side feature flags and role checks are presentation, not security. And a third-party script inside your origin is inside your boundary — which is why CSP, SRI and sandboxed frames exist.
The framing that "the client is untrusted, including code you wrote" is what makes the rest follow rather than needing to be memorised case by case.
MediumBest practice
A product requirement says users can format comments with bold and links. What is your policy for rendering that safely?
Do not accept HTML if you can avoid it — accept a restricted markup format (a markdown subset) and render it yourself, so the set of producible elements is decided by your renderer rather than by the input. If HTML is unavoidable, sanitise with a maintained library against an explicit ALLOW-list of tags and attributes, never a deny-list, and do it server-side as well as client-side because a client-only pass is bypassed by calling the API directly. Validate link hrefs against an allow-list of schemes so `javascript:` and `data:` cannot slip through, and add `rel="noopener noreferrer"` on external links. Layer CSP on top so anything that does get through cannot load an external script. Store the original input and sanitise on output, so you can change the policy later without having destroyed the source.
Two details are where teams usually go wrong. Allow-lists rather than deny-lists, because a deny-list is a bet that you thought of every dangerous construct, and the attacker only has to find one you missed. And sanitising on output rather than on input, because sanitising on the way in destroys the original — when the policy later turns out to be wrong, or too strict, there is nothing left to re-render. Client-side sanitisation alone is not a control at all: the attacker posts to the API directly and the payload is stored already clean-looking.
HardCode review
Review this post-login redirect
const next = new URLSearchParams(location.search).get('next');
if (next) location.href = next;
This is an open redirect: an attacker sends `?next=https://evil.example/login` and your own domain bounces the freshly-authenticated user to a convincing phishing page — the link looks legitimate because it starts on your origin. Scheme payloads like `javascript:` are also accepted here, which is script execution. Fix by never trusting the value as a URL: accept only a same-origin, root-relative path (starts with a single `/`, not `//` or `/\`), reject anything containing a scheme or control characters, and ideally validate it against a known set of routes. Falling back to a safe default when validation fails is better than attempting to sanitise a hostile value.
The `//evil.com` and backslash forms are what defeat naive checks, because browsers treat them as another origin while a `startsWith("/")` test passes.
EasyConcept
Why is "the client is untrusted" the starting point for every frontend security decision?
Because the user controls the entire runtime. They can read your bundle, edit variables in devtools, replay and modify any request, disable your JavaScript entirely and call the API directly with curl. So anything the client enforces is a convenience, not a control: hiding a button, disabling a field, validating a form, or checking a role in the UI all improve the experience and prevent none of them from acting. The practical consequence is a division of labour — the client validates for helpful feedback and the server validates for safety; the client hides what is irrelevant and the server decides what is permitted. Every frontend security question reduces to this, which is why it is worth stating explicitly rather than learning case by case.
The division of labour is the actionable part. It is not that client-side checks are worthless — they are the difference between a usable product and a hostile one — it is that they are never the control. A useful test when reviewing any frontend security measure: ask what happens if the user simply calls the endpoint directly with curl. If the answer is "nothing bad", the control is on the server where it belongs; if it is "they get in", the check was decoration.
HardDebugging
A password-reset token keeps appearing in third-party logs. How is it leaking?
// email link
https://app.example.com/reset?token=eyJhbGciOi…
Query strings leak through channels the application does not control: the URL is recorded in browser history, in server, proxy and CDN access logs, and it is shared whenever a user copies the link out of the address bar. The `Referer` header still leaks it to SAME-ORIGIN subresources, and to third parties on any page that loosens the referrer policy — modern browsers default to `strict-origin-when-cross-origin`, which trims cross-origin referrers to the origin, so that channel is narrower than it used to be but is not something to rely on. Mitigations: deliver the token in the URL FRAGMENT, which is never sent to servers at all; or have the landing page exchange it via POST immediately and strip it with `history.replaceState`. Also set `Referrer-Policy: no-referrer` on that route, make the token single-use and short-lived, and bind it to the requesting session so a leaked token is far less useful.
The durable lesson is that a secret in a URL is a secret in a log. Even with referrer trimming, the token is written to history, to every access log on the path, and into whatever the user pastes into a chat — none of which the application controls or can purge. That is why the fix is structural: keep it out of the query string, and make the token worth little by the time anyone reads a log.
EasyFollow-up
You said validation must happen on the server. Is client-side validation then pointless?
No — it just serves a different purpose. Client-side validation is a user-experience feature: it gives immediate feedback, avoids a round trip for an obvious mistake, and lets you guide someone through a form field by field. Server-side validation is the security and integrity control, because it is the only one the user cannot skip. They are not redundant, they are two jobs that happen to share rules. The practical implication is to share the schema between them where you can, so the two cannot drift, and to treat a client-side rule that has no server counterpart as a bug rather than an optimisation.
This is a common over-correction: told that the server must validate, people conclude the client check is theatre and drop it, which makes the product worse for every honest user. The two are different jobs that happen to share rules, and the practical risk is drift — the client accepts something the server rejects, so the user gets a confusing failure after submitting a form that looked valid. Sharing the schema between both is what keeps them honest.
MediumMultiple choice
What distinguishes stored, reflected and DOM-based XSS?
Stored is saved and served to everyone who views it (the most damaging). Reflected comes straight back from a crafted URL. DOM-based never reaches the server — client code writes untrusted input into the DOM — which is why server-side filtering alone misses it.
ExpertPredict the output
Which of these survive a naive `<script>`-stripping sanitiser?
// 1
<img src=x onerror=alert(1)>
// 2
<a href="javascript:alert(1)">x</a>
// 3
<scr<script>ipt>alert(1)</script>
// 4
<svg><animate onbegin=alert(1) attributeName=x></svg>
All four. None relies on a literal `<script>` tag surviving the filter.
1 and 4 use event-handler attributes; 2 uses a scheme payload in an href; 3 exploits the stripper itself — removing the inner `<script>` splices the remaining fragments into a valid tag. This is why allow-list sanitisation with a real HTML parser is the only defensible approach: a deny-list of tags or a regex is defeated by the shape of the input, not by cleverness.
The remaining 16 Web Security questions — plus mock interviews, spaced revision and progress tracking — are in the free interview prep workspace.