AlgoInsight

Free lesson · Frontend System Design

Security is a way of thinking, not a list of fixes

Medium10 min readfrontendsystem-design

Most engineers first meet web security as a checklist: escape this, add that header, set this cookie flag. The checklist isn't wrong, but it's the output of security thinking, not the thinking itself - and memorizing outputs without the reasoning behind them produces engineers who apply fixes ritualistically, miss the cases the checklist didn't anticipate, and can't reason about a novel situation. This cluster takes the harder but more durable approach: it teaches frontend security as a threat model - a structured way of thinking about what you're protecting, who's attacking, where the boundaries are, and what could go wrong - so that the specific defenses (which we'll cover thoroughly) arrive as consequences of reasoning rather than rules to memorize. This opening lesson builds that threat-modeling lens, and establishes the one principle that governs all frontend security.

That principle, the foundation everything else rests on: the frontend is fundamentally untrusted territory, and you can never trust the client. Your frontend code runs on the user's machine - a device you don't control, in an environment that may be actively hostile, where every line of your code is fully visible and every byte of data can be inspected, modified, or replayed. This reframes the entire security problem and is the single most important idea in the cluster. Once you internalize "never trust the client," most frontend security decisions follow logically, and the ones that don't are usually the bugs.

What a threat model is

Threat modeling is the practice of thinking like an attacker before one shows up. It has a few components, and walking through them for the frontend orients the whole cluster:

Assets - what are you protecting? You can't secure everything equally, so you start by naming what matters. For the frontend, the assets are primarily the user and their data: their session and identity (don't let it be hijacked), their personal data (don't let it leak), their actions (don't let them be forged), and their device and experience (don't let it be abused). Notice the frontend's assets are mostly about protecting the user - which, as we'll see, is different from protecting the server's secrets.

Threats - who or what is attacking, and how? You enumerate the ways things go wrong: an attacker injecting malicious script (XSS), tricking the user's browser into making forged requests (CSRF, clickjacking), intercepting data in transit, compromising a third-party dependency, exfiltrating data. Each is a threat, and the defenses exist to counter specific threats.

Attack surface - where can attacks enter? Every place untrusted input meets your code is attack surface: URL parameters, form inputs, data from APIs, third-party scripts, postMessages from other windows, the contents of storage. Reducing and guarding the attack surface is much of the defensive work.

Trust boundaries - where does trust change? A trust boundary is a line where data crosses from a less-trusted zone to a more-trusted one, and it's exactly where security checks belong. The frontend's key boundaries: between other origins and yours (the Same-Origin Policy line), between third-party code and your code (the dependency you didn't write running with your privileges), and most importantly between the client and the server (the line where untrusted client input reaches your trusted backend).

graph TD
    subgraph Untrusted["Untrusted zone"]
        Attacker[Attackers]
        ThirdParty[Third-party code]
        OtherOrigins[Other origins]
        ClientInput[Anything from the client]
    end
    subgraph Assets["Assets to protect"]
        Identity[User session / identity]
        Data[User's personal data]
        Actions[User's actions/integrity]
    end
    Untrusted -.->|cross trust boundaries| Assets
    Principle[NEVER TRUST THE CLIENT<br/>enforce security on the server] --> Assets
This diagram lays out the frontend threat model. On one side is the untrusted zone - attackers, third-party code, other origins, and anything coming from the client. On the other are the assets to protect: the user's session and identity, their personal data, and the integrity of their actions. Threats cross trust boundaries from the untrusted zone toward the assets. The governing principle, never trust the client, means security must ultimately be enforced on the server, because the client is untrusted territory. The frontend's security job is largely about protecting the user as data crosses these trust boundaries.

Why "never trust the client" is the master principle

This deserves elaboration because so much follows from it. The frontend runs in an environment you don't own and can't secure:

The decisive consequence: security must ultimately be enforced on the server, which is the one environment you control. The frontend cannot be the security boundary for protecting your system - it's on the wrong side of the trust line. This is why "I'll validate it on the client" is never a complete security answer, and why "the button is hidden so they can't do it" is not access control. Stating "client-side checks are UX, not security - the server must enforce everything, because we can't trust the client" is one of the strongest, most fundamental things you can say in a security discussion, and it's the lens to apply to every frontend security question.

So what is the frontend's security job?

If the server enforces security, you might ask what the frontend's security responsibilities even are - and the answer reframes the cluster. The frontend's job is protecting the user within the browser: preventing malicious code from running in your origin's context (XSS), preventing the user's browser from being weaponized against them (CSRF, clickjacking), keeping the user's data safe in transit and storage, and not introducing vulnerabilities through how you handle untrusted input and third-party code. These are real, serious responsibilities - they're just different from "protect the server's secrets," which the frontend structurally cannot do. The cluster's topics map onto this: XSS, CSRF, CORS, CSP, and the rest are all about protecting the user and the integrity of the experience in the untrusted browser environment.

Defense in depth and security as tradeoffs

Two more threat-modeling ideas complete the lens:

Defense in depth. Good security layers multiple defenses so that no single failure is catastrophic. You don't rely on output encoding alone to stop XSS - you also add a Content Security Policy, use HttpOnly cookies so a successful XSS can't steal tokens, and so on. Each layer catches what the others miss. When you study each defense in this cluster, hold in mind that it's one layer, valuable precisely because the others might fail.

Security is a tradeoff, not an absolute. Perfect security is unattainable and, even if it were possible, would make the product unusable (the most secure app is one that does nothing). So security is always a risk-based tradeoff against usability, development cost, and functionality. The threat-modeling approach makes these tradeoffs deliberate: you assess what's at stake, how likely and damaging a threat is, and how much friction a defense adds, then decide. This decision framing - "here's the threat, here's the risk, here's the defense and its cost, here's the tradeoff I'd make" - is exactly why this cluster teaches threat modeling rather than a checklist, because real security work is judgment under tradeoffs, not rule-following.

Important: Frontend security is a threat model, not a checklist: identify assets (the user's identity, data, and actions), threats (XSS, CSRF, interception, supply chain), attack surface (everywhere untrusted input enters), and trust boundaries (other origins, third-party code, and above all client→server). The master principle is never trust the client: all frontend code is visible (no secrets in the browser), all client-side checks are bypassable (client validation is UX, not security), so security must be enforced on the server. The frontend's actual job is protecting the user in the hostile browser - and good security is defense in depth (layered) and a deliberate risk-based tradeoff, not an absolute.

Interview: Open any security discussion by establishing the threat-model lens and the master principle: "I'd think about this as a threat model - what we're protecting (the user's session, data, and actions), who's attacking, and where the trust boundaries are. The foundational rule is that we never trust the client: all our frontend code is visible and any client-side check can be bypassed, so client validation is just UX and the server must enforce security. The frontend's job is protecting the user - preventing malicious script in our origin, stopping forged requests, keeping data safe - with layered defenses, since no single one is enough." Leading with "never trust the client" and the threat-model framing signals you understand security as reasoning, not a list.

Two more principles: least privilege and secure defaults

Beyond "never trust the client," two principles round out the security mindset and recur throughout the cluster:

Principle of least privilege. Grant any component - a script, a dependency, an iframe, an API token, a user role - the minimum access it needs to do its job, and no more. The less privilege something has, the less damage it can do if compromised. This shapes many specific defenses: a third-party script granted narrow permissions can't run wild if it's malicious; an API token scoped to one operation can't be abused for others; an iframe sandboxed to the minimum can't break out. When you study the upcoming defenses (CSP restricting what runs, Permissions-Policy restricting what features pages can use, iframe sandboxing), recognize them as least-privilege applied to specific surfaces. The question least privilege always asks: "does this thing actually need this access?" - and the default answer should be no until justified.

Secure by default. The safe configuration should be the default, with insecurity requiring a deliberate, visible opt-in. A framework that escapes output by default (so you'd have to explicitly reach for dangerouslySetInnerHTML to be unsafe) is secure-by-default; one that requires you to remember to escape is insecure-by-default and will eventually be forgotten. Good security design makes the secure path the easy, automatic one and the insecure path the conspicuous, deliberate one - which is exactly why frameworks name their escape hatches scarily. When designing a system, push the defaults toward safety so that forgetting something fails closed (denied) rather than open (allowed).

A worked threat model

Threat modeling is concrete once you apply it, so let's walk the lens over a feature - say, a comment system where users post comments that other users read. The exercise demonstrates how the framework generates the right defenses rather than recalling them:

Notice that we derived the defenses by reasoning about assets, boundaries, and threats - not by recalling a checklist. That's the entire value of the threat-model approach: confronted with any feature, you can reason from "what's the untrusted input, where does it cross a trust boundary, what could an attacker do there" to the appropriate defenses, including for features no checklist anticipated. In an interview, walking this reasoning for the feature at hand is dramatically stronger than reciting "use HTTPS, escape output, set headers" with no connection to the actual risk.

Security mindset mistakes to avoid

A few mindset errors are common enough to name, because recognizing them is part of thinking securely:

Security through obscurity. Believing something is safe because it's hidden or hard to find - an unlinked admin URL, obfuscated JavaScript, a "secret" parameter. Obscurity is not security; anything reachable can be found, and anything in the client can be read. Real security doesn't depend on the attacker not knowing how the system works (assume they know everything - they can read your frontend) but on controls that hold even when they do.

Trusting client-side validation for security. The recurring error: treating the browser's validation as a security control. It's UX; the server must re-check. An attacker bypasses your form entirely.

Treating security as a one-time task. Security isn't a phase you complete; it's an ongoing property. New dependencies introduce new risk, new features open new attack surface, and new vulnerabilities are discovered in existing code. The mindset is continuous vigilance - which is why later lessons cover dependency monitoring and why threat modeling is a habit, not a checkbox.

Note: The security mindset rests on a few recurring principles you'll see applied throughout the cluster: never trust the client (enforce on the server), least privilege (minimum access, so compromise is contained), secure by default (the safe path is automatic, insecurity is a deliberate opt-in), defense in depth (layers, so no single failure is fatal), and no security through obscurity (assume the attacker knows everything, since they can read your frontend). These aren't separate from the specific defenses ahead - they're the reasoning that generates them.

What you should carry out of this lesson

Frontend security is best approached as a threat model: name the assets (the user's identity, data, and the integrity of their actions), the threats (XSS, CSRF, clickjacking, interception, supply-chain compromise), the attack surface (everywhere untrusted input enters), and the trust boundaries (other origins, third-party code, and especially client→server). The master principle is never trust the client - frontend code is fully visible (no secrets survive there) and every client-side check is bypassable (client validation is UX, not security), so security must ultimately be enforced on the server. The frontend's real job is protecting the user within a hostile browser, using defense in depth (layered defenses) and treating security as a deliberate risk-based tradeoff rather than an absolute.

With the lens established, the cluster works through the specific threats and defenses - each understood as a consequence of this model. We begin with the threat that sits at the top of nearly every frontend security ranking, the one that most directly violates the "your origin's context" trust boundary by running an attacker's code as if it were yours: Cross-Site Scripting. It's next.

Back to Frontend System Design