← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 46 — Security for Front-End Developers

Section 1 — Security is a front-end job too

Security is often talked about as if it belonged only to back-end developers, system administrators, or specialists, but front-end developers make security decisions every day, whether they realise it or not. Every time you display data that came from a user, build a link from an input, store a value in the browser, or send a form, you are handling trust. The browser is where visitors type personal details, read private messages, and make decisions based on what they see. If the front end displays untrusted data carelessly, or sends it without thought, visitors can be misled, their sessions can be abused, or their information can leak, even when the server behind the site is well protected.

Front-end security is mostly about handling untrusted input with care and about not creating openings that a dishonest party could use. You do not need to learn how to break into systems to do this work well. In fact, this chapter teaches defence only: how to display data safely, how to use browser protections correctly, how to avoid storing secrets where they do not belong, and how to report a problem responsibly if you find one. That defensive focus is deliberate. A front-end developer's job is to build trustworthy interfaces, not to practise attacks, and the skills in this chapter are the ones you will use on every project, from a portfolio to a client site.

A useful starting point is to notice where trust flows through a front-end project. Data arrives from many places: what a visitor types, what comes back from a server, what is stored in the browser, what is read from an address, and what is supplied by a third-party script or embed. Each source deserves a different level of caution. Data you wrote yourself, in your own code, is relatively trustworthy. Data from a visitor, from another site, or from a public feed is not, until it has been handled safely. Treating all outside data as untrusted by default is not paranoia; it is a simple rule that prevents a large class of mistakes, because you stop assuming that input will be well behaved.

Finally, front-end security is also about communication. When a form fails, the message you show can either help a legitimate visitor or leak information that helps an attacker. When a session ends, the way you clear stored data matters. When a feature asks for a permission, the explanation you give affects whether a visitor grants it wisely. Security, in this sense, is part of user experience: clear, honest interfaces that protect people without confusing them. As you work through this chapter, notice how often the safest choice is also the clearest choice. That overlap is not a coincidence; interfaces that are honest about what they do are easier to keep safe.

Check: Why should a front-end developer treat data from visitors as untrusted by default?

Section 2 — Think like a defender, not an attacker

Thinking like a defender means asking, for each feature, what could go wrong if input is unexpected, and how the interface should behave safely in that case. It does not mean learning to attack systems or practising break-ins. A defender's questions are practical: if a visitor types something unusual into this field, will it display safely? If a link comes from an untrusted source, will clicking it do something unintended? If a stored value is changed by someone else, will the site still behave sensibly? Those questions can be asked and answered without ever attempting an attack, and they lead directly to safer code, clearer messages, and better tests on your own projects.

A defender also thinks in terms of layers. No single protection is perfect, so responsible sites combine several: safe display of data, sensible browser settings, validation on the server, short-lived sessions, and clear sign-out behaviour. If one layer fails or is bypassed, another may still protect the visitor. That layered thinking explains why this chapter covers many small habits rather than one grand solution. Escaping output, using safe browser features, setting cookie flags, and validating on the server each address a different risk, and together they make a site much harder to misuse than any one of them alone. When you review a project, look for missing layers, not just for a single flaw.

Another defender habit is to consider the visitor's perspective, especially a visitor who is less technical than you. An attacker may try to trick a visitor into clicking a misleading link or granting a permission they do not understand. Your interface can reduce that risk by using clear labels, showing the destination of important actions, and avoiding designs that train visitors to click without thinking. For example, a button that says what it will do, in plain language, is safer than a vague button that asks for trust. A warning that explains the consequence of an action, rather than merely saying something is dangerous, helps a visitor make an informed choice. Good security design respects the visitor's judgement instead of replacing it with jargon.

Finally, a defender knows the limits of front-end work. The browser is the visitor's device, and anything in the browser can be inspected or changed by the visitor. That means the front end can guide, warn, and validate for convenience, but it cannot be the final authority on sensitive decisions. The server must check again, and the design must assume that front-end checks can be bypassed. Accepting that limit is not defeatist; it is what lets you design honestly. You build the clearest, safest interface you can, you use every browser protection available, and you leave enforcement of important rules to the trusted side. That division of labour is the foundation of the rest of this chapter.

Check: What does thinking in layers mean in front-end security?

Section 3 — Trust boundaries: browser versus server

A trust boundary is a line between a part of a system you control and trust, and a part you do not. In web development, the most important trust boundary for a front-end developer is between the browser and the server. The server is, in principle, under your control or your team's control: you decide what code runs there, what data it stores, and what rules it enforces. The browser is on the visitor's device: the visitor can inspect its code, change its stored values, disable its scripts, and send requests that did not come from your interface at all. That does not make visitors untrustworthy as people; it means the browser, as a technical environment, cannot be trusted to enforce rules on their behalf.

This boundary explains many decisions that otherwise seem puzzling. Why must a server check a form again, even though the browser already checked it? Because a request can be sent without using the browser form at all, so the browser check is only a convenience. Why must a server check who is allowed to see private data, even though the interface hides that data from other users? Because a visitor could ask the server directly, bypassing the interface's hiding. Why must a price, a permission, or a role never be decided solely by front-end code? Because front-end code is delivered to the visitor and can be changed there. In each case, the server is the trusted enforcer, and the front end is the helpful guide.

Understanding the boundary also helps you design better interfaces. When you know the server will check again, you can make the browser check friendly and immediate, catching typos and missing fields before submission, without pretending it is a security barrier. When you know private data must be protected on the server, you can design the interface to request only what the current visitor is allowed to see, so the display matches the permission, rather than hiding data that is actually still being sent. When you know stored browser values can be changed, you can treat them as preferences or temporary state, not as proof of identity or permission. Those design choices feel natural once the boundary is clear.

A practical way to apply this idea is to ask, for each piece of data and each decision, on which side of the boundary should it live? If it is a display preference, a draft, or a filter choice, the browser is a fine home. If it is a secret, a permission, a price, or a record that affects other people, it belongs on the trusted side, with the browser only displaying what the server has approved. Drawing that line, feature by feature, is one of the most valuable exercises in this chapter, and it will serve you on every project, long after the specific tools you use have changed.

Check: Why can a browser check never be the final authority on a sensitive decision?

Section 4 — Never trust user input

Never trust user input is a short rule with a wide meaning. It does not mean you should distrust visitors as people; it means you should never assume that input will be in the form you expected, within the range you expected, or free of characters that change how your code behaves. A visitor might type a name that is unusually long, paste text copied from elsewhere that contains formatting, enter a number where you expected text, or submit a form with a field left blank. Some of that input is innocent, some is mistaken, and some may be deliberately crafted to test your site. Your code must handle all of it safely, without breaking, leaking information, or displaying something unintended.

The rule applies to every source of input, not just forms. It applies to text typed into a search box, to values read from an address, to data loaded from a file, to content pasted into a rich-text field, and to data returned by an external service. Each of those can contain characters that have special meaning in HTML, in an address, or in a database query on the server. If your code inserts input into a page, a link, or a request without handling those characters safely, the input can change the meaning of the surrounding code. That change of meaning is the heart of many front-end security problems, and it is why later sections focus so heavily on safe display and safe construction of addresses and attributes.

Handling input safely has two parts, and they serve different purposes. The first part is validation for usability: checking that a required field is present, that an email address looks plausible, that a number is within a sensible range, and giving the visitor a clear message when it is not. That helps legitimate visitors succeed. The second part is safe handling for security: ensuring that whatever input remains, after validation, is displayed and transmitted in a way that cannot change the structure of your page or request. That protects visitors even when input is unusual or hostile. Both parts matter, but they are not substitutes for each other. Validation that rejects obvious mistakes does not make unsafe display safe, and safe display does not make a missing validation message helpful.

A good habit is to treat input as data, not as code, at every step. Data is something to display, store, or compare; code is something to execute. Whenever you are tempted to turn input directly into HTML, into a script, or into a command, pause and ask whether there is a data-only way to achieve the same result, such as setting text content rather than HTML content. That question, asked consistently, prevents most input-related problems before they start. Never trusting input is therefore not a slogan; it is a design discipline, applied field by field, page by page, until it becomes automatic.

Check: What is the difference between validating input for usability and handling it safely for security?

Section 5 — What cross-site scripting means

Cross-site scripting, usually shortened to XSS, is a class of problem in which untrusted data is displayed on a page in a way that lets it be treated as code rather than as plain text. The name is historical and a little confusing, but the idea is simple: a site intends to show a visitor's comment, name, or search term as text, but because of unsafe handling, the browser interprets part of that data as markup or script, and then runs it in the context of the site. From the visitor's perspective, the page looks like the trusted site, but the injected content can alter what they see, interact with page elements, or trigger actions they did not intend. That mismatch — trusted appearance, untrusted content — is what makes this class of problem serious.

It helps to understand why the browser behaves this way. Browsers are designed to interpret HTML and to run scripts from the page they loaded. They cannot, on their own, tell the difference between markup written by the site's developer and markup that arrived inside user data, unless the developer makes that distinction explicit by handling data safely. If a developer inserts user data into a page as if it were HTML, the browser will faithfully interpret it as HTML, including any markup it contains. The browser is not malfunctioning; it is doing exactly what it was told to do. The fault lies in the handling, not in the browser, and the fix lies in ensuring that user data is always treated as data, never as code to be interpreted.

XSS matters for front-end developers because the display step is often their responsibility. Whether data comes from a form, a comment, a profile field, or a server response, the moment it is placed into the document is the moment its handling matters. A developer who displays data safely, using the techniques in the next sections, prevents this class of problem at its source. A developer who inserts data as raw HTML, because it is convenient or because it preserves formatting, creates an opening that may not be visible during normal testing, because normal test data rarely contains markup. Testing with ordinary names and comments will not reveal the problem; only a disciplined habit of safe display will prevent it.

Finally, keep perspective. XSS is not magic, and it is not a reason to fear displaying user data altogether. Sites display user data every day, safely, by following a few consistent rules: treat input as text by default, use safe templating, escape output when HTML is genuinely needed, and use additional browser protections as a second layer. Those rules are learnable, testable on your own projects, and covered in detail in the sections that follow. Understanding what XSS means — untrusted data mistaken for code — gives you the mental model to apply those rules correctly, rather than memorising them as disconnected tips.

Check: In plain language, what goes wrong in cross-site scripting?

Section 6 — Stored, reflected and DOM-based XSS in concept

Security discussions often divide cross-site scripting into three broad patterns: stored, reflected, and DOM-based. These are not three different kinds of magic; they are three different journeys that untrusted data can take before it is unsafely displayed. Understanding the journey helps you find where to apply a safe handling step, because the fix is always at the display or handling point, regardless of where the data came from. You do not need to practise any of these patterns to understand them, and this section describes them only at concept level, so you can recognise and prevent them in your own work.

In the stored pattern, untrusted data is saved somewhere, such as in a database or a shared list, and later displayed to visitors, possibly many visitors over a long time. A comment, a profile description, or a product review are typical examples of stored data. If that data is displayed unsafely, every visitor who views the page may be affected, which is why stored problems are often treated as especially serious. The defensive lesson is that data does not become trustworthy merely because it was stored: it must still be displayed safely at the moment it is shown, every time, no matter how long it has been stored or who originally submitted it.

In the reflected pattern, untrusted data is not stored at all; it is included in a request, such as a search term in an address, and immediately shown back on the resulting page, for example on a search results page that echoes what was searched for. If that echoed data is displayed unsafely, it can be interpreted as code in the visitor's browser. The defensive lesson is similar: data does not become safe because it came from the current visitor's own request. A search term typed by one person could have arrived via a link they clicked, and the page must still display it as text, not as markup.

In the DOM-based pattern, the problem happens entirely in the browser, through JavaScript that reads untrusted data, such as a value from the page address, and inserts it into the document unsafely. No server storage or reflection is involved; the front-end code itself creates the opening by how it handles data it read. This pattern is especially relevant to front-end developers, because it is created and fixed in front-end code. The lesson is to treat values read from the address, from storage, or from any external source with the same caution as form input, and to insert them using safe methods. In all three patterns, the defence is the same habit: display data as data, never as code. In all three patterns, the defence is the same habit: display data as data, never as code, every time.

Check: What is the main difference between stored and reflected patterns, in concept?

Section 7 — Why escaping output matters

Escaping output means converting characters that have special meaning in HTML into harmless representations, so that they are displayed as text rather than interpreted as markup. For example, a character that would normally start a tag is converted into a form that the browser displays literally, as that character, instead of treating it as the beginning of an element. When output is properly escaped, a visitor can type any characters they like, including characters that look like markup, and the page will show exactly what they typed, as text, without changing the page's structure. That is the goal of safe display: what the visitor sees matches what was submitted, as text, and nothing more.

Escaping matters because HTML is a language of special characters. A small set of characters controls structure: they open and close tags, delimit attributes, and separate elements. If user data containing those characters is inserted into HTML without escaping, the browser cannot distinguish the user's characters from the developer's markup, and it will interpret them together. The result may be a broken layout, a changed page structure, or, in the worst case, content that behaves as code. Escaping restores the distinction: the developer's markup remains markup, and the user's characters remain text. It is a simple idea, but it is the foundation on which safe display is built.

Where escaping happens depends on the tools you use. Many modern templating systems and frameworks escape output by default, which means that when you insert a value into a template, it is automatically treated as text unless you explicitly ask for HTML. That default is a valuable safety net, and you should prefer tools and settings that provide it. When you work without such a system, or when you build HTML strings by hand, you must escape values yourself, carefully and consistently, for the context in which they appear. Escaping for HTML text is not the same as escaping for an attribute value or for an address, and using the wrong kind of escaping for a context leaves a gap. Later sections explain those contexts separately.

A common mistake is to rely on validation alone instead of escaping. Validation may reject obviously problematic input, but it cannot guarantee that all future input will be safe, especially when data comes from many sources, is edited over time, or is displayed in a new context that the original validation did not anticipate. Escaping at output, by contrast, protects the display regardless of what the input was, because it is applied at the moment the data meets the page. The two practices complement each other: validate to help legitimate users and to reject nonsense, and escape to ensure that whatever is displayed is displayed safely. Together, they form a reliable habit for handling untrusted data.

Check: What does escaping output do to characters that have special meaning in HTML?

Section 8 — Text content versus HTML content

When you use JavaScript to place data into a page, you have a choice between treating that data as text and treating it as HTML, and that choice is one of the most important security decisions a front-end developer makes. Setting text content tells the browser: display this exactly as text, with no interpretation. Setting HTML content tells the browser: interpret this as markup, build elements from it, and run any behaviour it describes. For user data, or any data you did not write yourself, text content is almost always the correct choice. It is simpler, it is predictable, and it cannot turn data into code.

A concrete example makes the difference clear. Suppose you have an element intended to show a visitor's display name, and the name is stored in a variable. Assigning that variable to the element's text content will display the name as written, even if it contains characters that look like markup. Assigning the same variable as HTML content will cause the browser to interpret any markup-like characters, changing the page structure in ways the visitor may not have intended and the developer certainly did not intend. The visual difference may be invisible for ordinary names, which is why the mistake survives testing: it only becomes visible when input contains special characters. Safe code must be safe for unusual input, not just for ordinary input.

There are legitimate cases for HTML content, such as rendering rich text that an editor deliberately produced, or inserting markup that your own code generated from trusted templates. In those cases, the HTML is not raw user input; it is content that has been through a careful process, such as sanitisation, which a later section explains at concept level, or it is markup written by the developer. The rule is therefore not never use HTML content, but never use it for untrusted data without a deliberate, reviewed safety process. If you find yourself reaching for HTML content because it is convenient for displaying a name, a comment, or a search term, stop and use text content instead. Convenience is not a safety process.

To build the habit, review your code for places where you insert data into the document and ask, for each one, whether the data is being treated as text or as HTML. Change every untrusted insertion to text content, and document any remaining HTML insertions with a note explaining why they are safe and what process protects them. That review, done once per project and repeated when new features are added, catches the most common display problem in front-end work. Text by default, HTML only by deliberate exception, is a rule you can remember and apply on every project.

Check: When displaying a visitor's name, why is text content safer than HTML content?

Section 9 — The danger of inserting HTML strings

Building HTML by joining strings together is a common beginner habit, and it is one of the easiest ways to create an unsafe display. The pattern looks innocent: you write a template string containing markup, insert a variable holding user data into it, and assign the result as HTML content. For ordinary data, the result looks correct. For data containing markup-like characters, however, the result changes meaning, because the browser interprets the inserted data as part of the markup, not as text within it. The danger is not that the code looks suspicious; it is that it looks normal while handling data unsafely.

The problem is compounded by the fact that string-built HTML is hard to review. When markup and data are mixed in a single string, a reader must mentally separate the developer's tags from the inserted values, and must check whether each value has been escaped for its context. In a long template with several insertions, that check is easy to miss, especially when the code is edited later by someone who adds a new variable without realising it needs escaping. A template system that escapes by default removes much of that burden, because the separation between markup and data is built into the tool, and a developer must deliberately opt out to create HTML content. String building, by contrast, opts into danger by default.

There are safer alternatives for most cases. If you only need to display a value, set text content, as the previous section explained. If you need to build a list or a card from data, create elements with code and set their text content individually, rather than building a single HTML string for the whole card. That approach is slightly more verbose, but it is explicit: each piece of data is placed as text, and the structure is created by your code, not by interpreting data. If you genuinely need rich formatting, use a templating system that escapes by default, and reserve raw HTML insertion for content that has passed a deliberate safety process, which later sections describe.

To find this habit in your own projects, search for places where you assign HTML content from a string that includes a variable. For each one, ask whether the variable could ever contain untrusted data, including data from a server, from storage, or from an address, not just from a form. If the answer is yes, or even possibly, rewrite it using text content or a safe templating method. That search, done regularly, is a practical security review that requires no special tools. Building HTML from strings is not forbidden in all cases, but it is a pattern that demands justification, review, and safe handling every time, and for most display tasks, a safer pattern is both available and clearer.

Check: Why is HTML built from strings hard to review for safety?

Section 10 — Safe templating habits

Safe templating means using a system that separates the structure of a page from the data displayed in it, and that treats data as text unless you explicitly request otherwise. In a templating system, you write a template containing placeholders for values, and the system fills those placeholders when rendering. If the system escapes by default, a value containing markup-like characters will be displayed as text, not interpreted as markup, without you having to remember to escape each value by hand. That default is the single most valuable feature a templating system can offer for security, and it is why choosing a system with safe defaults matters more than memorising escaping rules.

Safe templating habits start with trusting the default. When a framework or library escapes output automatically, do not bypass that protection because a value is not displaying as you expected. Instead, ask why it is not displaying correctly: perhaps the value genuinely contains markup that should be displayed as text, in which case the escaped display is correct, or perhaps you are trying to render rich content that needs a separate, reviewed process. Bypassing escaping to make a display look right in a test case often creates an opening that only appears with real, varied input. The correct response to an unexpected display is to understand the data and the context, not to disable the protection.

Context also matters in templating, even with safe defaults. A value inserted into ordinary text, a value inserted into an attribute, and a value inserted into an address are in different contexts, and each requires appropriate handling. A good templating system understands those contexts and escapes accordingly, but not every system does so perfectly, and hand-built templates do not do so at all. When you insert a value into an attribute, such as a title or an image description, check that the system escapes for that context, and use quotes around the attribute value consistently. When you insert a value into an address, check that it is encoded for that context, and do not build addresses by joining untrusted strings without encoding. Those context checks are the second layer of safe templating, after the default itself.

Finally, document any exception where you deliberately display HTML content. Write a short comment explaining what the content is, where it came from, what safety process it passed through, and who reviewed it. That comment serves two purposes: it forces you to justify the exception at the time you write it, and it warns a future editor not to reuse that pattern for untrusted data without the same process. Safe templating is therefore both a tool choice and a discipline: choose defaults that protect you, respect those defaults, check contexts, and document exceptions. Projects that follow those habits display user data safely as a matter of routine, rather than by luck.

Check: What should you do before bypassing a templating system's automatic escaping?

Section 11 — Sanitising rich text: concepts only

Rich text is text that carries formatting, such as bold, italics, links, and lists, and it presents a harder problem than plain text, because the formatting itself is markup. A comment system that allows bold and links, or an editor that lets users format a description, must accept some markup while rejecting markup that could change page behaviour or introduce unsafe content. Sanitisation is the process of cleaning rich text by allowing only a known, safe set of elements and attributes, and removing or escaping everything else. It is a concept you should understand, even if you never implement a sanitiser yourself, because it explains how rich-text features can be offered safely, and why they should never be offered by simply inserting user markup as HTML.

The key principle of sanitisation is allowlisting, not blocklisting. An allowlist names exactly what is permitted — for example, a few formatting elements and safe attributes — and everything not on the list is removed. A blocklist, by contrast, tries to name everything dangerous and remove it, while allowing everything else. Blocklists fail because there are many ways to express dangerous content, including through attributes, addresses, and combinations that a blocklist author did not anticipate. Allowlists fail safe: if a new, unexpected form of markup appears, it is removed by default, because it was never allowed. That difference is why security guidance consistently prefers allowlists for rich text, and why you should be suspicious of any rich-text solution that claims to remove only known bad patterns.

Sanitisation is also context-sensitive and difficult to get right by hand. A link element may be safe with an ordinary web address but unsafe with an address that triggers script behaviour or points to an untrusted destination. An attribute that is harmless on one element may be dangerous on another. A well-maintained sanitisation library, written and reviewed by specialists, handles those distinctions, and it is updated as new issues are discovered. Writing your own sanitiser with simple string replacements is not a safe substitute, because it will almost certainly miss cases that a maintained library handles. If your project needs rich text, use a reputable library, configure its allowlist narrowly for your needs, and keep it updated, just as you would any other dependency.

For many projects, the safest choice is to avoid rich text altogether. Plain text, displayed with text content, meets the needs of names, comments, descriptions, and search terms in most cases, and it avoids the entire sanitisation problem. Offer rich text only when the feature genuinely requires formatting, and when you can justify the added complexity and maintenance. When you do offer it, test it on your own project with a variety of inputs, including unusually formatted text, and confirm that only the intended formatting survives. Rich text is a feature with a security cost, and understanding that cost helps you decide whether it is worth paying.

Check: Why is an allowlist safer than a blocklist for cleaning rich text?

Section 12 — Attributes and URLs are also injection points

Most discussions of unsafe display focus on text inside an element, but attributes and addresses are also places where untrusted data can change meaning if handled carelessly. An attribute is a value inside an element's opening tag, such as the description of an image or the title of a link. An address is the destination of a link or a resource. In both cases, the browser interprets special characters according to the context, and untrusted data that is inserted without appropriate handling can break out of its intended place and alter the element or its destination. Safe display therefore requires attention to every context where data appears, not only to visible text.

Consider an attribute first. If you build an element by joining a user value into an attribute without quoting and escaping it correctly, a value containing a quote character can end the attribute early, and what follows may be interpreted as a new attribute or as markup. Even with quoting, a value must be escaped for the attribute context, because characters that are harmless in text can still have special meaning inside an attribute. The safe habit is to avoid building attributes from strings altogether where possible: create the element with code and set the attribute value through the browser's property or method, which treats the value as data rather than as markup to be parsed. When you must use a template, ensure it escapes for attributes and that you use consistent quoting.

Addresses present a similar issue. A link destination built by joining an untrusted value into an address can be changed in ways that mislead visitors, such as pointing to a different site than the label suggests, or using a scheme that triggers behaviour other than ordinary navigation. The safe habit is to validate and encode address values for their context, to allow only the schemes your feature genuinely needs, such as ordinary web addresses, and to treat user-supplied destinations with suspicion, especially when they will be displayed as clickable links. For links to external sites, consider showing the destination clearly or warning the visitor, so they can judge whether to follow it.

The general lesson is to audit every insertion point, not just the obvious ones. When you review a page, list every place where data appears: text, attributes, addresses, and any other context your code uses. For each one, ask how the data is handled for that specific context, and whether a safer, data-only method exists. That systematic review catches problems that a focus on text alone would miss, and it builds a habit of thinking in contexts, which is the hallmark of careful front-end security work. That systematic review catches problems that a focus on text alone would miss.

Check: Why can a quote character in an attribute value be dangerous if not handled correctly?

Section 13 — Content Security Policy in plain language

Content Security Policy, usually called CSP, is an additional browser protection that lets a site declare, through a header or a meta element, which sources are allowed to load scripts, styles, images, and other resources. In plain terms, it is a set of rules the site gives the browser, saying, for example, only load scripts from this site itself, or only load images from these named sources. If a page tries to load a resource from a source not on the list, the browser blocks it. That blocking acts as a second layer of defence: even if some unsafe content somehow reaches a page, CSP may prevent it from loading or executing, because it does not come from an allowed source.

CSP is best understood as a safety net, not as a replacement for safe display. Safe display prevents untrusted data from becoming code in the first place; CSP limits the damage if that prevention fails, by restricting what code and resources the page may use. A site that relies on CSP alone, while still displaying data unsafely, is not secure, because CSP policies can be complex, can have gaps, and can be bypassed if they are too permissive. A site that displays data safely and also uses a sensible CSP is more resilient, because an attacker would need to overcome both layers. That layered reasoning, introduced earlier, is exactly why CSP is worth learning, even though it is configured on the server or host rather than in front-end code alone.

A simple CSP starts strict and is relaxed only where necessary. For a static site that loads no third-party scripts, a policy that allows resources only from the site itself is a strong starting point. As features are added, such as fonts, analytics, or embeds, each new source must be added deliberately, with an understanding of what it is allowed to do. Adding a broad permission, such as allowing scripts from any source, defeats the purpose of the policy, and should be avoided even when it makes a feature work quickly. The correct approach is to add the narrowest permission that makes the feature work, test it, and document why it was added, so future editors do not broaden it casually.

Implementing CSP requires care and testing, because an overly strict policy can break legitimate features, such as inline styles or scripts that a site depends on. Test a new policy on a preview or staging site first, watch the browser console for blocked resources, and adjust narrowly until the site works correctly. Also remember that CSP is set by the server or host, so a front-end developer may need to work with whoever controls hosting to configure it. Understanding what CSP does, and being able to ask for a sensible policy, is a valuable skill even when you do not set the header yourself. It turns a mysterious header into a deliberate, understandable layer of protection.

Check: Why is CSP described as a second layer, not a replacement for safe display?

Section 14 — What CSP can and cannot do

Content Security Policy can do a great deal to limit what a page is allowed to load and execute, but it cannot do everything, and understanding its limits prevents false confidence. CSP can block scripts, styles, images, and other resources from sources that are not on its allowlist, which reduces the impact of unsafe content that reaches a page. It can restrict which sites may embed your page, which helps against certain framing problems discussed later. It can require secure connections for resources, and it can report violations, giving you information about attempted loads that were blocked. Those are powerful capabilities, and they make CSP a worthwhile addition to a production site.

CSP cannot, however, fix unsafe display on its own. If a site displays user data as HTML without escaping, and the resulting content comes from an allowed source or uses only allowed mechanisms, CSP may not block it, because the policy judges sources, not the trustworthiness of every piece of data. Similarly, CSP cannot protect data that is already being sent to an allowed but compromised third party, cannot fix a weak password or a leaked secret, and cannot prevent a visitor from being tricked into performing an action on another site. It is a browser enforcement mechanism for resource loading, not a general solution to every security problem. Treating it as such would leave other risks unaddressed.

CSP also has practical limits that affect how it is used. Policies are complex, and a small mistake, such as omitting a source that a legitimate feature needs, can break a site in ways that are confusing to diagnose. Policies must be maintained as features change: every new font, analytics tool, or embed requires a deliberate policy update, and forgetting to update the policy can either break the feature or tempt a developer to broaden the policy excessively to make it work. Older browsers may support CSP incompletely, so it should not be the only protection for a site whose visitors include older devices. Those limits do not make CSP useless; they make it a layer to be managed carefully, alongside safe display and server-side checks.

The balanced view is to use CSP as part of a set of defences, with realistic expectations. Set a policy that is as strict as your site's features allow, test it thoroughly on preview and staging, monitor violation reports for unexpected blocks, and review it whenever you add a new source. At the same time, continue to display data safely, validate on the server, and follow the other practices in this chapter. A developer who understands both what CSP can do and what it cannot do will neither ignore it nor over-rely on it, and that judgement is more valuable than any single policy setting.

Check: Give one thing CSP can block and one thing it cannot fix on its own.

Section 15 — Cookies: how the browser stores them

A cookie is a small piece of data that a site asks the browser to store, so it can be sent back to that site on later visits. Cookies are how the web remembers things across separate requests, which are otherwise independent: a site can recognise a returning visitor, remember a preference, or keep a visitor signed in, because the browser stores a cookie and includes it with future requests to that site. The server sets a cookie by including an instruction in its response, and the browser stores it, subject to rules about which site it belongs to, how long it should last, and when it should be sent. Those rules are central to front-end security, because cookies often carry session information.

It is important to understand that cookies are tied to a domain and a path. A cookie set by one site is normally sent only to that site, not to other sites, which is a basic privacy and security boundary. The browser also respects an expiry time: some cookies last only for the browsing session and are discarded when the browser closes, while others persist for days, months, or longer, until they expire or are deleted. A site can also set a cookie for a subdomain, allowing related services under the same domain to share it, which is convenient but must be done deliberately, because it widens the set of services that receive the cookie. Understanding those boundaries helps you reason about who can read a cookie and when.

From a front-end perspective, the most important distinction is between cookies that JavaScript can read and cookies that it cannot. By default, a cookie set by a server can be read by JavaScript running on that site, which is useful for preferences but dangerous for sensitive session data, because any script running on the page, including a third-party script, could read it. A flag called HttpOnly, explained in a later section, tells the browser not to expose the cookie to JavaScript at all, so it is sent only with requests to the server. That flag is a server setting, not a front-end code choice, but a front-end developer should understand it and be able to ask for it when session cookies are involved.

Cookies also raise privacy questions that go beyond security. A site that sets many cookies, especially from third parties, can track visitors across sites, and visitors increasingly expect clear explanations of what is stored and why. As a front-end developer, you should know what cookies your site sets, why each one exists, and whether it is truly needed. Remove cookies that serve no purpose, prefer short lifetimes for sensitive cookies, and ensure that privacy information on the site accurately describes cookie use. Cookies are a small, powerful mechanism, and treating them with care — knowing their scope, lifetime, and accessibility — is a core part of responsible front-end work.

Check: Why does it matter whether a cookie can be read by JavaScript?

Section 16 — SameSite cookies in plain language

SameSite is a cookie attribute that controls when a browser sends a cookie with requests that come from another site, and it is one of the most important modern defences against a class of problems called cross-site request forgery, which is explained in later sections. In plain terms, a request is same-site when it originates from the same site that set the cookie, and cross-site when it originates from a different site. SameSite tells the browser whether to include the cookie in cross-site requests, in some cross-site requests, or in none at all. That control matters because many attacks rely on the browser automatically including a session cookie with a request triggered from another site, making the request look as if the signed-in visitor intended it.

SameSite has three main values, and understanding them in concept is enough for a front-end developer. Strict means the cookie is sent only with same-site requests, which is the most protective setting, but it can break legitimate flows where a visitor arrives from another site, such as following a link from an email to a signed-in page, because the cookie will not be sent on that first arrival. Lax is a middle setting: the cookie is sent with top-level navigation, such as following a link, but not with requests triggered in the background by another site, which covers many common cases while preserving most protection. None means the cookie is sent with cross-site requests as well, which is sometimes needed for embedded services, but it must be paired with the Secure flag, so the cookie is only sent over HTTPS, and it should be used only when cross-site sending is genuinely required.

Choosing a SameSite value is a trade-off between protection and functionality, and it is usually decided on the server, where cookies are set. A front-end developer's role is to understand the trade-off, to test flows that involve arriving from another site, and to report breakages accurately. If a signed-in feature stops working when a visitor follows a link from elsewhere, SameSite Strict may be the cause, and the fix may be to use Lax for that cookie, rather than to remove the protection entirely. If a feature genuinely requires cross-site cookies, such as an embed that must recognise a signed-in visitor, the discussion should be explicit, with Secure enabled and with an understanding of the added risk.

Finally, remember that SameSite is a browser defence, not a complete solution. It reduces the risk of cross-site requests abusing a session, but it does not replace server-side checks, such as requiring a token for state-changing actions, which later sections explain. It also depends on browsers supporting the attribute, which modern browsers do, but older behaviour may differ. Treat SameSite as a valuable layer: ask for a sensible setting on session cookies, test cross-site arrival flows, and do not assume that setting it alone makes a site immune to forgery. Layers, again, are the theme: SameSite plus server checks plus clear interface design together provide far stronger protection than any one alone.

Check: In plain language, what does SameSite Strict prevent a browser from doing?

Section 17 — HttpOnly and Secure cookie flags in concept

Two cookie flags, HttpOnly and Secure, are simple settings with significant protective effects, and a front-end developer should understand both, even though they are set by the server. HttpOnly tells the browser not to expose the cookie to JavaScript running on the page. The cookie is still sent automatically with requests to the site that set it, so sign-in continues to work, but scripts cannot read its value. That matters because if a page ever displays untrusted data unsafely, a script running in that context could otherwise read a session cookie and send it elsewhere. HttpOnly does not fix unsafe display, but it limits what such a script can steal, which is a valuable second layer.

Secure tells the browser to send the cookie only over HTTPS connections, never over plain HTTP. That matters because a cookie sent over plain HTTP could be read by anyone positioned on the network between the visitor and the server, such as on a shared public network. By restricting the cookie to HTTPS, the Secure flag ensures it travels only in encrypted form. For any site that uses HTTPS, which should be every production site, setting Secure on session cookies is a straightforward, sensible choice. A cookie without Secure on an HTTPS site is an unnecessary risk, and a cookie on a plain HTTP site cannot be protected by this flag at all, which is another reason HTTPS is a baseline.

Together, these flags illustrate the layered approach again. HttpOnly protects a cookie from scripts, Secure protects it in transit, and SameSite, covered previously, protects it from cross-site sending. None of them, alone, makes a session fully secure, and none of them replaces server-side checks. A session cookie with all three flags set sensibly is much harder to abuse than one with none, but it can still be abused if the server accepts forged requests without additional checks, or if the visitor's device itself is compromised. Understanding the flags lets you ask the right questions when a session is designed: are session cookies HttpOnly, are they Secure, and what SameSite value do they use? If the answer to any of those is no without a clear reason, that is a finding worth raising.

From a front-end perspective, there is also a practical consequence: because HttpOnly cookies cannot be read by JavaScript, your front-end code should not depend on reading a session cookie to decide whether a visitor is signed in or what they may do. Instead, the interface should rely on what the server tells it, through responses and page data, and should handle sign-in state through server-provided information rather than by inspecting cookies directly. That design is cleaner as well as safer: the front end displays what the server has approved, and the server enforces the session. When you review a project, check whether front-end code tries to read session cookies directly; if it does, that is a sign the design may need rethinking, and that HttpOnly may not be set.

Check: What does HttpOnly prevent JavaScript from doing with a cookie?

Section 18 — What cross-site request forgery means

Cross-site request forgery, usually shortened to CSRF, is a problem in which a visitor who is signed in to a trusted site is tricked into making a request to that site that they did not intend, through an action triggered from another site. The key insight is that browsers automatically include cookies with requests to a site, even when the request was triggered from elsewhere, unless protections like SameSite prevent it. That automatic inclusion is normally convenient: it keeps a visitor signed in as they navigate. It becomes a risk when a request that changes something — such as updating a setting, transferring a value, or deleting an item — can be triggered without the visitor's knowledge or consent, simply because they visited a malicious page while still signed in elsewhere.

It helps to think of CSRF in terms of intention. A legitimate request is one the visitor intended to make, through an interface they understood, on the site they thought they were using. A forged request is one that looks, to the server, as if it came from that visitor, because it carries their session cookie, but that the visitor did not intend, because it was triggered from a different site or context. The server cannot, from the cookie alone, tell the difference between those two cases. That is why additional defences are needed: the server must check something that a cross-site trigger cannot easily provide, such as a token that only the legitimate site's pages include, or a header that only a genuine request from the site would carry. Those defences are explained at concept level in the next section.

CSRF matters for front-end developers because the interface is where intention is created. A form or a button that clearly says what it will do, that requires a deliberate action, and that is served from the legitimate site, helps establish intention. Conversely, an action that can be triggered by simply visiting a page, or by clicking a disguised element, undermines intention. Front-end developers should also understand which actions are sensitive: any action that changes data, settings, permissions, or ownership deserves protection, while merely viewing a page generally does not, because viewing does not change state. That distinction — reading versus changing — guides where CSRF defences are most needed.

Finally, keep CSRF in proportion. It is not a reason to distrust every link or to fear the web, and it is not a problem that front-end code alone can solve. It is a known class of problem with known defences, most of which live on the server, supported by browser settings like SameSite and by careful interface design. Your role is to understand the concept, to recognise which actions are state-changing and therefore sensitive, to ensure that forms for those actions are built in a way that supports server defences, and to test that sensitive actions cannot be triggered accidentally. That understanding, combined with the cookie protections already covered, gives you a solid grasp of this important topic.

Check: In plain language, why cannot a server tell a forged request from a legitimate one using only a cookie?

Section 19 — CSRF defences at concept level

CSRF defences work by giving the server a way to distinguish a request that genuinely came from its own pages from a request that was triggered from elsewhere. The most common defence, at concept level, is a token: a unique, unpredictable value that the server includes in its own forms and pages, and that the browser sends back when the form is submitted or the action is performed. A cross-site trigger, which does not have access to the legitimate page's content, cannot easily obtain that token, so its forged request will lack it, and the server can reject the request. The token is not a password the visitor types; it is a secret shared between the server and its own pages, checked on every state-changing action.

A second defence is to check the origin or referrer of a request, meaning where the request claims to have come from. If a request to change a setting claims to come from a different site, or has no plausible origin, the server can reject it. That check is useful, but it is not sufficient on its own, because origin information can be missing in some legitimate cases, such as when a visitor's browser or network suppresses it, and because it depends on headers that are not always present. Responsible sites therefore combine origin checks with tokens, rather than relying on either alone. The layered pattern appears again: two independent checks are harder to bypass than one.

A third defence is the SameSite cookie setting, already covered, which prevents the browser from sending session cookies with many cross-site requests in the first place. If the cookie is not sent, the forged request arrives without a session, and the server treats it as unauthenticated. SameSite is powerful, but it is a browser defence with trade-offs, as discussed, and it should complement tokens, not replace them. A site that uses SameSite Lax or Strict, plus tokens for state-changing actions, plus origin checks where appropriate, has a robust set of defences that covers most cases.

From a front-end perspective, the practical implications are clear. Forms that change data should be built as genuine forms served by the site, including whatever token the server provides, and should use the POST method for changes, not a simple link that can be triggered by visiting a page. State-changing actions should never be performed by a plain GET request, such as a link that deletes an item when clicked, because GET requests are easy to trigger cross-site and are meant for reading, not changing. JavaScript that performs state-changing requests should include the token or header the server expects, following the server's documented pattern. Those habits, combined with server defences, make CSRF a manageable, well-understood risk rather than a mysterious threat.

Check: At concept level, how does a CSRF token help a server recognise a genuine request?

Section 20 — Same-origin policy and CORS in concept

The same-origin policy is a fundamental browser rule that limits what a page from one origin can do with data from another origin. An origin is defined by the combination of scheme, host, and port — for example, the secure site at example.com on its standard port is a different origin from a site at another domain, even if both are on the web. Under the policy, a page can generally only read data from its own origin, unless the other origin explicitly permits it. That restriction is what prevents a page you happen to have open from quietly reading your private data on another site you are signed in to, simply because both pages are open in the same browser.

It is important to understand what the policy does and does not prevent. It prevents reading data across origins, but it does not, by itself, prevent sending a request across origins, which is why CSRF defences are still needed. A page can trigger a request to another site, and the browser may include cookies with it, even though the page cannot read the response. That distinction — sending versus reading — explains why the same-origin policy and CSRF are related but different topics. The policy protects confidentiality across origins; CSRF defences protect the integrity of actions, by ensuring that a request was genuinely intended. Both are needed, and neither replaces the other.

Cross-Origin Resource Sharing, known as CORS, is the mechanism by which a server can relax the same-origin policy in a controlled way. When a front-end application needs to read data from another origin, such as a public data service, the server at that origin can include headers saying which origins are allowed to read its data, and for which methods. The browser enforces those headers: if the origin is allowed, the page can read the response; if not, the browser blocks the read, even though the request may have been sent. CORS is therefore a permission system, controlled by the data's owner, not by the requesting page. A front-end developer cannot grant themselves CORS permission by changing their own code; the server that owns the data must grant it.

For a front-end developer, the practical lesson is to expect CORS errors when you first try to read data from another origin, and to understand that the fix lies with the data owner, not with a trick in your code. Workarounds that bypass the browser's enforcement, such as disabling security features in your own browser for testing, do not fix the problem for visitors and should never be shipped. Instead, use a server or a proxy that is authorised to fetch the data, or ask the data owner to allow your origin, or choose a data source that already permits your use. Respecting the same-origin policy and CORS is not an obstacle to development; it is the mechanism that keeps data on one site private from pages on another.

Check: What is the difference between sending a cross-origin request and reading its response?

Section 21 — Authentication tokens: where not to put them

An authentication token is a value that proves a visitor is signed in, and where it is stored in the browser has significant security consequences. Tokens are often stored in one of three places: local storage, session storage, or a cookie. Each place has different accessibility: storage that JavaScript can read is convenient for front-end code, because the code can attach the token to requests itself, but it is also readable by any script running on the page, including a third-party script or, in the worst case, script introduced through unsafe display. A cookie with the HttpOnly flag, by contrast, cannot be read by JavaScript, but is sent automatically by the browser, which is both convenient and safer against script theft, provided other protections like SameSite are also set.

The general guidance, at concept level, is to avoid storing sensitive authentication tokens in storage that JavaScript can read, unless you have a clear reason and have weighed the risks. A token in local storage persists across sessions and is readable by any script on the site, which widens the impact if a script problem ever occurs. A token in session storage is limited to the current tab and session, which is slightly narrower, but it is still readable by scripts on the page. An HttpOnly cookie, set by the server, is not readable by scripts, and when combined with Secure and SameSite, it provides a stronger default for session authentication. That is why many responsible sites prefer HttpOnly cookies for sessions, even though it requires the server to handle authentication rather than the front end managing a token directly.

There are cases where a front-end application does use a token in storage, such as when it talks to a separate data service that expects a token header. In those cases, the design should be explicit about the trade-off: the token should have a short lifetime, limited permissions, and a clear revocation path, and the site should use safe display and CSP to reduce the chance of script injection. The token should never be a long-lived, powerful credential stored casually in local storage and forgotten. Short-lived, narrowly scoped tokens, refreshed through a safer mechanism, are a more responsible pattern when token storage is genuinely needed.

As a front-end developer, you may not choose the authentication design yourself, but you should be able to recognise a risky one and raise it. If you see a long-lived token stored in local storage, or a token logged to the console, or a token included in an address where it may be recorded in history, those are findings worth discussing with whoever owns the back end. Authentication storage is a design decision with lasting consequences, and asking where a token lives, how long it lasts, and how it can be revoked is a valuable contribution, even when the final decision rests elsewhere.

Check: Why is an HttpOnly cookie generally safer than local storage for a session token?

Section 22 — Local storage, session storage and cookies compared

Browsers offer several ways to store data on a visitor's device, and choosing among them requires understanding their scope, lifetime, and accessibility. Local storage stores key-value pairs that persist across sessions, until explicitly cleared, and are accessible to JavaScript on the same origin. It is useful for preferences, drafts, and other non-sensitive data that should survive a browser restart. Session storage is similar, but its data lasts only for the current tab session and is discarded when the tab closes. It is useful for temporary state, such as a form draft during a single visit, that should not persist beyond that visit. Cookies, as covered earlier, are sent with requests to the server, have configurable lifetimes and flags, and can be made inaccessible to JavaScript.

The differences matter for security and privacy. Because local and session storage are readable by JavaScript on the origin, any script running on the page can read them, including third-party scripts you include. That makes them unsuitable for secrets, session tokens, or personal data that should not be exposed to every script. Cookies, when set with HttpOnly, are not readable by JavaScript, which makes them better for session data, but they are sent automatically with requests, which introduces CSRF considerations that storage does not have in the same way. No storage mechanism is universally safe; each is safe for some purposes and unsafe for others, and the choice should follow the data's sensitivity and purpose.

Lifetime is another distinction. Local storage has no automatic expiry, which means data stored there can linger indefinitely, even after a visitor has forgotten about it. That is convenient for a theme preference, but risky for anything sensitive, because a shared device may retain data long after the visitor leaves. Session storage's automatic clearing on tab close is safer for temporary data, but it is still readable during the session. Cookies can be given explicit expiry times, allowing a designer to choose a short lifetime for sensitive data and a longer lifetime for a harmless preference. When you choose a storage mechanism, also choose a lifetime, and prefer the shortest lifetime that still meets the feature's needs.

A practical rule for front-end developers is to store only what the feature needs, in the narrowest place that works, for the shortest time that works. Preferences and drafts belong in local or session storage, depending on whether they should persist. Session authentication belongs, where possible, in an HttpOnly cookie, not in storage. Sensitive personal data should not be stored in the browser at all unless there is a clear reason and a clear deletion plan. Reviewing a project's storage use, and asking for each item why it is there and how long it lasts, is a simple audit that catches many unnecessary exposures before they become problems.

Check: For a draft that should disappear when the tab closes, which storage is most appropriate?

Section 23 — Login forms: safe front-end habits

A login form is a sensitive interface, because it handles the credentials that prove a visitor's identity, and its design affects both security and usability. The first habit is to serve the form over HTTPS, always, and to submit it over HTTPS, so credentials travel encrypted. A login form on a plain HTTP page, or one that submits to a plain HTTP address, exposes credentials to anyone on the network, regardless of how well the rest of the site is built. Check the address and the form's submission target before launch, and treat any plain HTTP login as a blocking problem, not a minor issue to fix later.

The second habit is to handle credentials carefully in the browser. A password field should use the appropriate input type, so the browser masks the characters and offers to save the password securely in its own password manager, rather than in your site's storage. Do not store a password in local storage, session storage, or a cookie, and do not log credentials to the console, even during development, because logs can be copied, shared, or retained. Do not pre-fill a password field with a real value from the server, and do not display a password in plain text by default. If you offer a show-password option, make it an explicit, reversible choice by the visitor, clearly labelled, so they can check what they typed without exposing it by accident.

The third habit is to design helpful, non-leaking messages. When a login fails, the message should not reveal whether the username exists or the password was wrong, because that distinction helps an attacker enumerate accounts. A generic message, such as that the details do not match, is safer and still helpful enough for a legitimate visitor, who can try again or use a recovery flow. Similarly, a password recovery flow should not confirm whether an account exists, but should give a consistent response regardless, and should send any recovery link through a secure, time-limited channel. Those message choices are front-end decisions with security consequences, and they deserve the same care as the form's layout.

Finally, support good visitor habits through the interface. Allow password managers to work, by using standard field names and autocomplete values, rather than custom tricks that break them. Do not impose arbitrary password rules that frustrate visitors without improving security, such as requiring frequent changes or forbidding pasted passwords, which actually hinder password manager use. Offer clear sign-out, session expiry information, and a way to review active sessions if the back end provides it. A login form that is clear, honest, and respectful of visitor tools is both more usable and more secure than one that is clever, restrictive, or obscure.

Check: Why should a failed login message not say whether the username or the password was wrong?

Section 24 — Password fields and autocomplete

Password fields deserve attention because they are the interface through which visitors enter one of their most sensitive secrets, and small design choices affect both security and usability. The basic rule is to use the correct input type for passwords, so the browser masks the entry, offers its password manager, and handles the field with appropriate care. Masking prevents someone looking over a visitor's shoulder from reading the password, and password manager support encourages the use of long, unique passwords, because the visitor does not need to memorise or type them repeatedly. A custom text field styled to look like a password field, without using the correct type, loses both benefits and should be avoided.

Autocomplete attributes help the browser understand what a field is for, and using them correctly improves both security and usability. For a sign-in form, the username and password fields should be marked so the browser can offer the saved credentials for that site. For a registration or password-change form, the new password field should be marked as a new password, so the browser can suggest a strong password and save it correctly, rather than autofilling an old one into the wrong place. Those attributes are small, standard, and easy to get wrong by omission, but their effect is large: they determine whether password managers work smoothly or frustrate visitors into choosing weak, memorable passwords.

A show-password toggle is a useful feature when implemented thoughtfully. It lets a visitor check for typos, especially on a phone where typing is error-prone, without forcing them to submit and fail repeatedly. The toggle should be an explicit button, clearly labelled, that switches the field between masked and visible, and that returns to masked when the visitor moves on or submits. It should not default to visible, and it should not be a checkbox hidden in small print. The visitor should always know whether their password is currently visible, because that knowledge lets them decide whether it is safe to reveal it in their current surroundings.

Finally, never weaken password handling for convenience. Do not limit password length to a short maximum, do not forbid spaces or pasting, and do not display a password in an error message or a confirmation page. If a password is mistyped, ask the visitor to type it again, rather than showing what was entered. Those rules may seem minor, but they shape visitor behaviour: a form that respects password managers and allows long, pasted passwords encourages strong credentials, while a form that fights those tools encourages weak ones. Password fields are a small interface with an outsized impact, and getting them right is a mark of careful front-end work. Password fields are a small interface with an outsized impact on security.

Check: How does correct autocomplete help visitors use stronger passwords?

Section 25 — Validation on the client is not security

Client-side validation means checking input in the browser before it is sent, such as ensuring a required field is filled, a number is within range, or an email address looks plausible. It is valuable for usability, because it gives immediate feedback, prevents unnecessary server requests, and helps visitors correct mistakes quickly. It is not, however, a security control, because it runs on the visitor's device, where it can be bypassed, disabled, or ignored. A visitor can turn off JavaScript, edit the page in developer tools, or send a request directly without using the form at all, and in each case, client-side validation will not run. Treating it as security therefore leaves a gap that is easy to bypass.

That gap matters most for rules that protect other people or enforce important limits. For example, a browser check that a comment is no longer than a certain length, or that a price cannot be changed, or that a user may only perform an action a certain number of times, cannot be enforced by the browser alone, because the visitor controls the browser. If the server accepts whatever it receives without checking, those limits are not limits at all; they are suggestions that a determined visitor can ignore. The server must therefore repeat every important check, with the same rules, and reject input that fails, regardless of what the browser said. Client validation and server validation should agree, but only the server's version is authoritative.

Understanding this distinction improves interface design as well as security. When you know the server will check again, you can make client checks friendly and forgiving, focusing on helping legitimate visitors rather than on catching attackers. You can show clear messages, highlight the field that needs attention, and preserve the visitor's other entries so they do not have to start over. You do not need to make client validation adversarial or punitive, because its job is guidance, not enforcement. At the same time, you should design the interface to display server validation errors gracefully when they arrive, because a legitimate visitor may still trigger them, for example if a rule changed since the page loaded, or if they submitted from an older version of the page.

A useful test for any validation rule is to ask: if this check were removed from the browser, would the system still be safe? If the answer is yes, because the server checks the same rule, then the browser check is a usability improvement, correctly placed. If the answer is no, because only the browser checks it, then the rule is misplaced and must be added to the server before the feature is considered secure. That test, applied rule by rule, is a simple audit that every front-end developer can perform, and it prevents the common mistake of assuming that a friendly browser message is also a security barrier.

Check: Why can a visitor bypass client-side validation entirely?

Section 26 — Server validation as the real gate

Server validation is the checking that happens on the trusted side, after a request arrives, before any action is taken or any data is stored. It is the real gate for security, because the server is under your control and cannot be bypassed by editing the browser, disabling scripts, or sending a request directly. Every important rule — that a required field is present, that a value is of the expected type and within range, that a visitor is allowed to perform the requested action, and that a value does not contain characters that would be unsafe in its destination — must be enforced on the server, even if the browser already checked it for convenience. That duplication is not wasteful; it is the division of labour between usability and security.

Server validation also handles cases that the browser cannot know about. A browser may check that an email address looks plausible, but only the server can check whether that address is already in use, whether the visitor is allowed to change a particular record, or whether a value conflicts with existing data. A browser may check that a field is filled, but only the server can check that the submitted data is consistent with the visitor's session, permissions, and the current state of the system. Those checks require trusted data that the browser does not have, or should not be trusted to provide, which is why they belong on the server. Front-end developers should understand this, so they design interfaces that expect server errors and display them helpfully, rather than assuming browser checks are sufficient.

The relationship between client and server validation should be cooperative, not competitive. The browser checks first, to give immediate feedback and to reduce unnecessary requests. The server checks again, authoritatively, and returns clear errors when a check fails, so the interface can display them. If the two sets of rules disagree, visitors will be confused: a form that passes in the browser but fails on the server, with a vague message, feels broken, even though the server is behaving correctly. Keeping the rules aligned, and ensuring server errors are specific enough to be helpful without leaking sensitive information, is a joint responsibility of front-end and back-end work, and it requires communication between the people building each side.

Finally, server validation is where safe handling for storage and display begins. A server that validates input, escapes or encodes it appropriately for its destination, and stores it in a form that can be displayed safely, protects every future display of that data, on every page and for every visitor. A front-end developer who understands that chain — browser guidance, server gate, safe storage, safe display — can design interfaces that fit into it, rather than working against it. Validation on the server is not a back-end detail to ignore; it is the foundation on which front-end safety rests.

Check: Why must the server repeat a check the browser already performed?

Section 27 — Showing errors without leaking information

Error messages are a security surface, because they reveal information about a system, and the amount they reveal should be deliberate. A helpful error tells a legitimate visitor what went wrong and what to do next, without telling an attacker something useful about accounts, data, or system structure. A leaking error, by contrast, may confirm that an account exists, reveal the internal name of a database field, expose a file path, or describe a validation rule in a way that helps an attacker craft a bypass. The front-end developer's job is to design messages that are helpful to the visitor in front of them, while revealing as little as possible to someone probing the system.

The principle is to be specific about the visitor's action and vague about the system's internals. For a form field, it is helpful to say that a password must be at least a certain length, because that is a rule the visitor needs to follow, and it is already visible in the interface. It is not helpful, and it is risky, to say that a particular username exists but the password was wrong, because that confirms an account. For a page that fails to load, it is helpful to say that something went wrong and to offer a way to try again or contact support, but it is not helpful to display a detailed technical stack trace, which may reveal software versions and internal paths. The distinction is between information the visitor needs to succeed, and information only an attacker would find useful.

Error messages also need to be consistent. If a login form gives a generic message for a wrong password but a different message for a non-existent account, the difference itself leaks the information, even though neither message alone seems revealing. Similarly, if a recovery flow behaves differently for existing and non-existing accounts, in timing or in message, that difference can be observed. Consistency is therefore a security practice, not just a style choice: similar situations should produce similar messages and similar behaviour, so that an observer cannot learn by comparing cases. That consistency should be designed into the interface, not left to chance.

Finally, errors should be actionable and kind. A visitor who encounters an error is already frustrated, and a message that blames them, uses jargon, or offers no next step will make them leave. Offer a clear next step: correct a field, try again, check a connection, or contact support with a reference code that helps support staff find the detailed log, without showing that log to the visitor. That reference code pattern is valuable: the visitor gets a simple code to quote, support gets the detail it needs from a private log, and the public message stays clean. Helpful, consistent, non-leaking errors are a hallmark of a mature, trustworthy interface.

Check: Why should similar error situations produce similar messages?

Section 28 — Safe handling of query strings and fragments

A query string is the part of an address after a question mark, carrying extra information such as a search term or a filter choice. A fragment is the part after a hash, pointing to a section of a page and handled by the browser rather than sent to the server. Both are common sources of input for front-end code: a search page reads its query string to know what to search for, a filtered list reads its parameters to know which filter to apply, and a single-page application may read a fragment to know which view to show. Because those values can be crafted by anyone who creates a link, they must be treated as untrusted input, just like form data, and handled safely at every step.

The first safe habit is to display query and fragment values as text, not as HTML, using the same text-content rule that applies to form input. A search results page that echoes the search term should display that term as text, so that any markup-like characters in it are shown literally, not interpreted. A filter label derived from a query value should likewise be set as text. That single habit prevents the most common problem with query handling, which is the reflected pattern discussed earlier, where data from an address is shown back unsafely. Testing with ordinary search terms will not reveal the problem; only a habit of safe display, applied to address-derived values as well as form values, will prevent it.

The second habit is to validate and encode query values for their context. If a query value is used to build a request to a server, it should be encoded so that special characters do not change the request's meaning. If it is used to select a filter, it should be checked against the list of valid filters, and an invalid value should fall back to a safe default rather than being used blindly. If it is used to build a link, it should be encoded for an address context. Those steps are similar to form validation, but they are easy to forget for address values, because they arrive invisibly, without a form being submitted. Treating the address as an input form, with the same care, closes that gap.

Finally, avoid putting sensitive information in query strings or fragments. Addresses are recorded in browser history, in server logs, in referrer information sent to other sites, and in links that visitors share. A token, a password, or a private identifier placed in an address can therefore leak through channels that a form submission over HTTPS would not. If a feature needs to pass sensitive data, it should do so through a secure, server-handled mechanism, not through a visible address. Query strings and fragments are excellent for search terms, filters, and navigation state, which are meant to be shareable; they are poor homes for secrets, which are meant to be private.

Check: Why should a search page display the search term as text, not as HTML?

Section 29 — Open redirects and untrusted links

An open redirect is a feature that takes a destination from input, such as a parameter in an address, and redirects the visitor to that destination without checking whether it is safe or intended. It becomes a problem when an attacker can craft a link that appears to come from a trusted site, because it uses that site's domain, but that actually redirects the visitor to a different, untrusted site, where they may be tricked into entering credentials or downloading something harmful. The visitor's trust in the original domain is misused: they click a link that looks legitimate, because it starts with a familiar address, and they end up somewhere else entirely. That misuse of trust is why open redirects, even though they seem minor, are treated as a security issue.

The defensive principle is to never redirect to an untrusted destination based solely on input. If a feature genuinely needs to redirect after an action, such as returning a visitor to the page they came from after signing in, it should check the destination against a list of allowed destinations, or against a rule that only permits relative paths within the same site. A destination that points to another domain, or that uses an unusual scheme, should be rejected or replaced with a safe default, such as the site's home page. That allowlist approach, familiar from rich-text sanitisation, is safer than trying to block known bad destinations, because new tricks for disguising a destination can always be invented, while an allowlist fails safe by rejecting anything not explicitly permitted.

Front-end developers encounter redirects in several places: after sign-in, after form submission, in links that pass through a redirect service, and in single-page applications that navigate based on a parameter. In each case, ask whether the destination comes from input, and if so, whether it is checked. A link that a developer wrote by hand, pointing to a known page on the same site, is not an open redirect risk. A redirect that takes its destination from an address parameter, a form field, or a stored value that a visitor could influence, is a risk unless it is validated. That distinction — fixed destination versus input-driven destination — is the key to auditing redirects in a project.

Also consider how redirects affect visitor trust more broadly. A site that redirects visitors unexpectedly, or that sends them to external sites without warning, trains them to accept redirects without thinking, which makes them more vulnerable elsewhere. When a link genuinely goes to an external site, consider making that clear, either through its label, its context, or a brief interstitial that shows the destination and asks for confirmation for sensitive actions. Clear, predictable navigation is both good usability and good security, because visitors who understand where they are going are harder to mislead.

Check: Why is an allowlist safer than a blocklist for redirect destinations?

Section 30 — Clickjacking and framing in concept

Clickjacking is a class of problem in which a page is displayed inside a frame on another site, in a way that tricks a visitor into clicking something they did not intend. In concept, an attacker site embeds a trusted site in a frame, makes that frame invisible or disguised, and places its own content over it, so that a click the visitor thinks is on the attacker's page actually lands on the trusted page, performing an action there. The visitor believes they are interacting with one interface, but their click is delivered to another. That mismatch between perceived and actual target is what makes clickjacking deceptive, and it is why framing controls matter.

The defence, at concept level, is to control whether your site may be framed, and by whom. A site can send a header, or use a related policy, telling the browser not to allow the site to be displayed in a frame at all, or to allow it only in frames from the same origin or from named, trusted origins. For most sites, which do not need to be embedded in other sites, denying framing entirely is the simplest and safest choice. For sites that do offer an embed feature, such as a widget intended to be placed on other sites, framing should be allowed only for the specific origins that are meant to embed it, and the embedded content should be limited to what is safe to show in that context. That deliberate choice, rather than allowing framing by default, is the core of the defence.

Front-end developers should also think about the interface side of framing. If your site is legitimately embedded, ensure that the embedded view is clearly identifiable as your site, that sensitive actions in the embedded view require additional confirmation, and that the embedding site cannot disguise those actions. Sensitive actions, such as changing settings, confirming a purchase, or granting a permission, should generally require a full-page, clearly identified context, not a small, easily disguised frame. That design choice reduces the chance that a visitor will perform a sensitive action without realising where they are.

Finally, remember that framing controls are set by the server or host, through headers, but understanding them is a front-end responsibility, because the interface is what is being framed. When you review a site, ask whether it needs to be framed, and if not, whether framing is denied. If it does need to be framed, ask which origins are allowed and whether sensitive actions are protected in that context. Those questions, asked at design time, prevent a class of deception that is difficult to detect from the victim's perspective, because the victim believes they clicked something else entirely. Clear framing policy is a quiet, effective protection.

Check: In concept, how does clickjacking trick a visitor?

Section 31 — HTTPS as a security baseline

HTTPS was introduced in the deployment chapter as the secure version of the web protocol, and it returns here as a security baseline, because many front-end protections depend on it. HTTPS encrypts the conversation between the browser and the server, so that data in transit cannot be easily read or altered by someone on the network. It also authenticates the server, through its certificate, so the browser can verify it is talking to the site named in the address. Without HTTPS, a login form, a session cookie, or a private message could be exposed in transit, regardless of how carefully the front-end code was written. Encryption in transit is therefore not an optional enhancement; it is the foundation on which other protections stand.

Several browser features also require HTTPS, or a local development address, before they will work. Features that access sensitive capabilities, such as location, camera, or certain storage and service features, are restricted to secure contexts, because the browser does not want to grant powerful capabilities over an insecure connection that could be tampered with. A site that tries to use those features over plain HTTP will find them unavailable, which is a deliberate browser decision, not a bug. Designing for HTTPS from the start therefore avoids a class of surprises, where a feature works locally, over a secure local address, but fails in production because the production site is not secure.

For a front-end developer, the practical HTTPS habits are straightforward. Always use HTTPS addresses when linking to other sites, when loading resources, and when submitting forms. Never mix secure and insecure resources on a secure page, a problem covered in the next section as mixed content. When you deploy, confirm that HTTPS works on every address form, including the apex and www forms, and that the certificate covers all names you use. When you test, use a secure local setup where possible, so that secure-context features behave as they will in production. Those habits are simple, but they prevent a surprising number of security and functionality problems.

Finally, remember the limit of HTTPS, stated earlier: it protects the connection, not the honesty of the site at the other end. A secure connection to a dishonest site is still a dishonest site, and a padlock does not mean the content is true or the operator is trustworthy. HTTPS is a necessary condition for a trustworthy site, not a sufficient one. It must be combined with safe display, server validation, sensible cookie settings, and the other practices in this chapter. A site that is secure in transit, safe in display, and honest in content is one visitors can trust, and HTTPS is the first, indispensable layer of that trust.

Check: Why do some browser features only work over HTTPS or a local address?

Section 32 — Mixed content and why browsers block it

Mixed content occurs when a secure HTTPS page loads some resources over insecure plain HTTP, such as an image, a stylesheet, or a script from an HTTP address. The page itself is encrypted, but the mixed resources are not, which undermines the security the padlock promises. An attacker on the network could read or alter an insecure resource, and if that resource is a script or a stylesheet, altering it could change the behaviour or appearance of the secure page. Browsers therefore block certain kinds of mixed content, particularly scripts and stylesheets, and may warn about or block other kinds, such as images, depending on the browser and the resource type. That blocking is protective, even though it can make a page look broken.

The distinction between active and passive mixed content helps explain browser behaviour. Active content, such as scripts and stylesheets, can change the page's behaviour or appearance, so browsers block it outright on secure pages, because allowing it would let an insecure resource control a secure page. Passive content, such as images and videos, cannot directly change the page's behaviour in the same way, though it can still leak information about what the visitor is viewing and can be altered to show misleading content. Browsers may load passive mixed content with a warning, or may block it in stricter modes, but the direction is clear: mixed content is discouraged, and the safest approach is to eliminate it entirely, rather than relying on browser warnings to manage it.

For a front-end developer, the fix for mixed content is usually simple: change insecure addresses to secure ones. When you link to a resource, use an HTTPS address, and when you load a resource from another site, confirm that the site offers HTTPS for that resource. If a third-party resource is only available over HTTP, consider whether it is essential, whether an alternative secure source exists, or whether the resource can be hosted securely yourself, with appropriate rights. Also check for addresses that are constructed dynamically, such as from data or configuration, because a hard-coded secure address can still become mixed content if a variable supplies an HTTP address at runtime. A systematic check, using developer tools to list insecure requests, will reveal those cases.

Testing for mixed content should be part of every deployment check. Open the live HTTPS site, open developer tools, and look for warnings about insecure resources or blocked content. Fix each one by securing the address or removing the resource, and retest until no warnings remain. A page with no mixed content earns its padlock honestly: every resource it loads is encrypted, and visitors can trust that the page they see is the page you sent, not a mixture of secure and insecure parts. That completeness matters, because a single insecure script can undermine an otherwise secure page.

Check: Why do browsers block mixed scripts and stylesheets more strictly than mixed images?

Section 33 — Dependencies and supply-chain risk

A dependency is a library or tool that your project uses but did not write, such as a framework, a date helper, or a charting library. Dependencies save enormous time, because they let you use tested, maintained code instead of reinventing common features. They also introduce supply-chain risk, which is the risk that comes from trusting code written by others, distributed through a package system, and updated over time. If a dependency contains a vulnerability, or if a malicious version is published, or if a dependency you rely on is abandoned and no longer maintained, your site inherits that risk, even though you did not write the problematic code. Understanding that inheritance is essential for responsible front-end work.

Supply-chain risk has several forms. A dependency may have an honest vulnerability, discovered after release, which its maintainers fix in a later version; your site remains vulnerable until you update. A dependency may depend on other dependencies, creating a chain where a problem deep in the chain affects many projects that never directly chose that code. A package account may be compromised, or a maintainer may be tricked, leading to a malicious release that looks like a routine update. None of those scenarios requires you to have done anything wrong; they are consequences of reusing code at scale. The defence is therefore not to avoid dependencies, which would be impractical, but to manage them deliberately: choose them carefully, keep them updated, and know what you depend on.

Choosing a dependency well means looking beyond its features. Check whether it is actively maintained, with recent updates and responsive maintainers, whether it is widely used and reviewed, and whether its documentation is clear. Prefer well-known, focused libraries over obscure, sprawling ones for a simple task, and avoid adding a large dependency for a tiny feature that plain code could handle. Each dependency adds size, complexity, and risk, so the question is not only can this library do the job, but is it worth its cost in size, maintenance, and trust? That cost-benefit thinking, applied to every new dependency, keeps a project's dependency list lean and understandable.

Finally, keep an inventory of your dependencies and review it periodically. Know which libraries your project uses directly, and be aware that they bring their own dependencies with them. When a security advisory is published for a library you use, update promptly, test the site, and deploy the fix. When a library is abandoned, plan to replace it, rather than leaving an unmaintained component in a live site indefinitely. Dependencies are a form of trust, and like any trust, they should be granted deliberately, reviewed regularly, and withdrawn when they are no longer warranted. That discipline turns reuse from a hidden risk into a managed, valuable practice.

Check: Why does a vulnerability in a dependency affect your site even though you did not write that code?

Section 34 — Keeping libraries up to date

Keeping libraries up to date is one of the simplest and most effective security habits, because many updates exist specifically to fix vulnerabilities discovered after a previous version was released. When maintainers publish a fix, attackers can learn about the vulnerability from the public advisory and can target sites that have not yet updated. The window between a fix being published and a site applying it is therefore a period of known risk, and shortening that window is valuable. Regular updates, applied promptly and tested carefully, are a direct way to reduce exposure, and they are a habit that should be scheduled, not left to chance.

Updating is not, however, a matter of blindly accepting every new version the moment it appears. Updates can change behaviour, remove features, or introduce new bugs, and a major version change may require code changes in your project. A sensible update routine balances speed and caution: apply security fixes promptly, even if they require a small adjustment; apply routine updates regularly, in a test environment first, and check that key features still work; and plan major updates deliberately, reading the release notes and allowing time for testing. For a small project, that routine may mean checking for updates monthly, applying security fixes as soon as you learn of them, and testing on a preview deployment before promoting to production.

Automation can help, without removing judgement. Tools that notify you when a dependency has a new version, or that open a proposed update for review, reduce the chance that an update is forgotten. They do not replace testing: an automated update that breaks a feature and is deployed without checking is worse than a delayed update that is tested properly. Treat an automated update proposal as a prompt to test, not as an instruction to deploy. Run the site's key journeys on a preview, check for errors in developer tools, and confirm that the update does not change the visible behaviour in unintended ways. That test, even when brief, catches most update-related breakages.

Also keep a record of what you updated and when, in your project notes or version history, so that if a problem appears later, you can trace it to a change. If an update causes a problem that cannot be fixed quickly, rolling back to the previous version, as discussed in the deployment chapter, is a legitimate response, provided you understand the security trade-off of remaining on the older version and plan to re-apply the update with a fix. Keeping libraries up to date is not about chasing novelty; it is about maintaining a known, supported, and patched foundation, so that your site's security does not silently decay as the world around it changes.

Check: Why should a security fix be applied promptly, rather than at the next convenient time?

Section 35 — Secrets, API keys and the public bundle

A front-end bundle is the set of files delivered to the browser, and by definition, everything in it is public. That fact has a direct consequence for secrets and API keys: any key, token, or credential placed in front-end code, or in a file that the build includes, will be delivered to visitors and can be read by them. This point was made in the deployment chapter, and it returns here because it is also a security principle: the browser is not a safe place for secrets. A private key that is needed to call a paid service, to access a database, or to sign a request must never be embedded in front-end code, however the build is configured, because the build's output is the visitor's copy.

The confusion often arises with API keys that are described as public. Some services provide a key that is intended to be used in the browser, such as a key for a map or a public data feed, and that key is limited in what it can do, often restricted to a particular domain and to specific, low-risk services. That kind of key is different from a private key, and its documentation will say that it is safe for browser use, with restrictions you should apply. The distinction is in the documentation and in the permissions: a public, restricted key can be safely embedded, because its abuse is limited and it can be rotated; a private, unrestricted key cannot, because its abuse could be serious and its exposure is irreversible without revocation. When in doubt, read the service's documentation for the words public and private, and treat anything labelled private as forbidden in the browser.

If a feature needs a private key, the safe pattern is to keep the key on a server or in a trusted function that the front end calls. The front end sends the visitor's request to that trusted endpoint, the endpoint uses the private key to call the service, and it returns only the result the visitor is allowed to see. The key never travels to the browser, and the endpoint can enforce limits, such as rate limits or permission checks, that the browser cannot enforce. That pattern adds a server component, but it is the correct cost of using a private service from a public interface. Pretending the browser can hold a secret, by hiding it in an obscure file or encoding it, does not change the underlying fact that the browser must use it, and therefore must be able to read it.

Finally, audit your bundle before deployment for accidental secrets, by searching for key-like strings and for configuration files that may have been included by mistake. Use an ignore file to keep secret configuration out of version control, and provide example configuration with placeholders instead of real values. If you discover a secret in a deployed bundle, revoke it immediately and replace it, rather than merely deleting it from the next version, because copies may already exist. Secrets in the public bundle are a common, preventable mistake, and the habit of checking for them before every deployment will protect you and your users from its consequences.

Check: How can you tell whether an API key is safe to use in the browser?

Section 36 — Permissions: camera, location and notifications

Browser permissions control access to powerful capabilities, such as the camera, microphone, location, and notifications, and they exist because those capabilities can reveal sensitive information about a visitor. A site must request permission before using them, and the browser shows a prompt that the visitor can accept or decline. That prompt is a security boundary: it ensures that a site cannot silently access a camera or track a location without the visitor's knowledge. As a front-end developer, your job is to request permissions only when needed, to explain why they are needed, and to handle a refusal gracefully, without punishing the visitor or breaking the site.

The first habit is to request a permission in context, at the moment the visitor needs the feature, rather than on page load before they understand why. A visitor who taps a button to take a photo understands why the camera is being requested, and is more likely to grant it wisely, than a visitor who is asked on arrival, before they have seen any benefit. Explain the purpose in plain language, near the button or in the interface, before the browser prompt appears, so the visitor is not surprised by it. If the visitor declines, respect that decision: offer an alternative, such as uploading a photo instead of taking one, or entering a location manually instead of using automatic location, and do not repeatedly prompt them in a way that pressures them to change their mind.

The second habit is to use only the precision and duration you need. For location, a coarse location may be sufficient for a nearby search, while precise location is needed only for navigation or a very local feature. Requesting more precision than necessary increases privacy risk without improving the feature. Similarly, do not retain permission-based data longer than needed, and do not send it to a server unless the feature genuinely requires it. If a feature works entirely in the browser, such as showing a photo preview before upload, keep the data in the browser and do not transmit it until the visitor deliberately submits it. Those choices respect the visitor's privacy and reduce the impact if data is mishandled.

Finally, test permission flows on a real device, including the refusal case, because prompts and behaviour differ across browsers and devices. Check that the site remains usable when a permission is denied, that the interface does not get stuck waiting for a permission that will never be granted, and that the visitor can change their decision through browser settings, with guidance from your interface if needed. Permissions are a trust negotiation between a site and a visitor, and a site that asks sparingly, explains clearly, and respects refusal will earn more trust, and more grants, than one that asks eagerly and indiscriminately.

Check: Why should a permission be requested at the moment it is needed, not on page load?

Section 37 — File uploads from the front end

File uploads let visitors send files to a site, such as a photo, a document, or an avatar, and they introduce risks that front-end developers must understand, even though the main checks happen on the server. A file is not just its name or its displayed type; it is a collection of bytes that the server will store, process, or display to others. If a file is accepted without checks, it could be much larger than expected, in a format the site does not support, or contain content that is unsafe to display or to store. The front end can help by guiding visitors toward suitable files, but it cannot be the final gate, for the same reason client validation cannot be security: the browser is under the visitor's control.

Front-end habits for uploads start with clarity. Tell the visitor what kinds of files are accepted, what the size limit is, and what the file will be used for, before they choose a file. Use the file input's accept attribute to suggest suitable types in the file picker, while remembering that this is a convenience, not a security check, because a visitor can still select another type. Check the file's size in the browser before upload, and warn the visitor if it exceeds the limit, so they do not wait for a large upload only to be rejected. Show a preview where appropriate, such as for an image, so the visitor can confirm they chose the right file, but do not assume that a preview proves the file is safe; a preview is a display, and display must itself be safe.

Security checks for uploads belong on the server, and a front-end developer should understand them at concept level, so they can design interfaces that fit them. The server should check the file's actual type, not just its extension, enforce size limits, store files in a location that cannot execute them as code, and serve them with appropriate content types so they are displayed as files, not run as scripts. It should also consider privacy: an uploaded photo may contain metadata, such as location or device information, that the visitor did not intend to share, and a responsible site should strip or warn about such metadata where appropriate. Those server checks are beyond front-end code, but they affect what the front end should promise visitors about their uploads.

Finally, handle upload failures gracefully. If an upload fails because of size, type, or a network problem, show a clear message explaining the reason and what to do next, and preserve any other form data the visitor entered, so they do not have to start over. Do not show a generic error that leaves the visitor guessing, and do not silently discard a file while claiming success. Uploads are a moment of trust: a visitor is giving your site something of theirs, and a clear, honest, well-checked upload flow respects that trust, while a careless one risks both security and goodwill.

Check: Why is a file input's accept attribute not a security check?

Section 38 — Third-party scripts and embeds

Third-party scripts and embeds are code or content from another site that you include in your page, such as analytics, advertising, social media widgets, maps, videos, and fonts. They are powerful because they let you add complex features without building them yourself, but they also extend trust: a script you include runs with the same privileges as your own code on that page, and can read page content, access storage for your origin, and perform actions as if it were your code. An embed in a frame is more isolated, but it can still affect what visitors see and, depending on its configuration, interact with your page. Including a third party is therefore a security decision, not just a feature decision.

The first habit is to include only third parties you need and trust, and to understand what each one does. Before adding a script, ask what data it collects, what it needs to access, and whether a simpler alternative exists, such as hosting a font yourself or using a plain link instead of a widget. Each additional script increases page size, slows loading, and widens the set of parties that can affect your page. A page with many third-party scripts is harder to audit, harder to keep secure, and more fragile when one of those parties changes its code or goes offline. A lean page, with a few well-chosen third parties, is easier to understand and to protect.

The second habit is to load third-party resources securely and with appropriate restrictions. Load them over HTTPS, so they cannot be altered in transit, and consider whether a script needs to run with full privileges or whether it can be loaded in a more restricted way, such as in a sandboxed frame for an embed that does not need access to your page. For scripts, integrity checks, where the browser verifies that the script it received matches a hash you provided, can detect tampering, though they require the third party to keep its script stable. For embeds, sandbox attributes can limit what the embedded content may do, such as preventing it from running scripts or submitting forms, unless those capabilities are genuinely needed. Those mechanisms are not a substitute for trust, but they reduce the damage if a trusted party is compromised.

Finally, review third parties periodically, just as you review dependencies. A script that was safe when added may change its behaviour, start collecting more data, or become unmaintained. Check what third parties your pages actually load, using developer tools, and remove any that are no longer needed or whose purpose you cannot explain. Keep a list of third parties, their purpose, and their data practices, in your project notes, so a future editor can audit them without guessing. Third-party inclusion is a continuing responsibility, not a one-time addition, and managing it carefully protects both your visitors and your site's reputation.

Check: Why does including a third-party script extend trust to that party?

Section 39 — Privacy: collecting only what you need

Privacy in front-end development starts with a simple principle: collect only the data you genuinely need, for a purpose you can explain, and keep it for only as long as that purpose requires. That principle, often called data minimisation, is both a privacy practice and a security practice, because data you do not collect cannot be leaked, misused, or demanded. A form that asks for a phone number when an email address would suffice, a tracker that records detailed behaviour when a simple page count would answer the question, and a feature that stores location history when only a current location is needed, all violate that principle, even if they are implemented securely. The safest data is the data you never collected.

Applying minimisation requires asking, for each field, each tracker, and each stored value, what decision or feature it supports, and whether that feature could work with less data or with less precise data. A newsletter form needs an email address, not a full name, address, and phone number. A nearby search needs a location at the moment of search, not a history of locations. A preference, such as a theme choice, needs a single value stored in the browser, not a server record tied to an identity. Those questions, asked at design time, often reveal that a feature can be simplified without losing value, and simplification usually improves both privacy and usability, because visitors are more willing to share a small, understandable amount of data than a large, unexplained amount.

Transparency is the companion to minimisation. Tell visitors, in plain language, what you collect, why you collect it, how long you keep it, and who else, if anyone, receives it. That information belongs in a privacy page linked from the footer, and in short explanations near the point of collection, such as beside a form field or before a permission request. Avoid legalistic language that obscures meaning; a privacy explanation that a visitor cannot understand fails its purpose, even if it is technically complete. Also honour visitor choices: if a visitor declines a permission, opts out of analytics, or asks for their data to be deleted, respect that choice promptly and completely, within the limits of what the system allows.

Finally, remember that privacy is not only about what you collect, but about what you expose through your interface. A page that displays one visitor's data to another, a search that reveals private terms in its suggestions, or a shared device that retains personal data after sign-out, are all privacy failures, even if no data was stolen. Design interfaces to show each visitor only their own data, to clear sensitive data on sign-out, and to avoid displaying personal details unnecessarily. Privacy, like security, is a design discipline, applied field by field and page by page, and it earns the trust on which a lasting site depends.

Check: Why is data you never collect safer than data you collect securely?

Section 40 — Session expiry and sign-out behaviour

A session is the period during which a visitor remains recognised as signed in, and how it ends matters as much as how it begins. Session expiry means that after a period of inactivity, or after an absolute time limit, the session is no longer valid, and the visitor must sign in again. Sign-out means the visitor deliberately ends the session, and the site should clear the session on the server and remove any session data from the browser. Both mechanisms protect visitors, especially on shared devices, where a session left open could be used by the next person to sit down. A site that never expires sessions, or whose sign-out does not actually end the session, leaves visitors exposed long after they have stopped using it.

From a front-end perspective, the visible parts of session handling are sign-out, expiry messages, and the behaviour of the interface when a session ends. Sign-out should be easy to find, clearly labelled, and effective: after signing out, the visitor should no longer see private content, and navigating back should not restore it from a cache. If a visitor's session expires while they are working, the interface should explain what happened, preserve any draft they were working on where possible, and guide them to sign in again, rather than silently discarding their work or showing a confusing error. Those experiences are front-end design decisions, even though the session itself is managed on the server.

Expiry times involve a trade-off between security and convenience. A very short expiry, such as a few minutes, is secure but frustrating for a visitor who is reading a long page or writing a long message. A very long expiry, such as months, is convenient but risky on a shared device and after a device is lost. Responsible sites often use a combination: a longer absolute limit, plus a shorter inactivity limit, so that an actively used session can continue, while an abandoned session ends promptly. Sensitive actions, such as changing a password or a payment setting, may require re-authentication even within a valid session, as an additional check. Those choices are made on the server, but a front-end developer should understand them and design messages that explain them without jargon.

Finally, test sign-out and expiry on your own projects, including on a shared-device scenario. Sign in, sign out, and confirm that private pages no longer load, that the back button does not reveal them from cache, and that a new visitor cannot continue the previous session. Test what happens when a session expires mid-task, and ensure the visitor's work is handled kindly. Session ending is not an afterthought; it is the closing of a trust relationship, and handling it clearly and completely is a mark of a site that respects its visitors, from sign-in to sign-out.

Check: Why should sign-out clear the session on the server, not just hide private pages in the browser?

Section 41 — Protecting admin and account pages in UI

Admin and account pages are sensitive because they allow changes to data, settings, permissions, and sometimes other people's accounts. The front-end interface for those pages must therefore be designed with care, even though the real enforcement happens on the server. The first principle is to show each visitor only what they are allowed to see and do. If a visitor is not an administrator, they should not see admin links, admin pages, or admin actions in the interface, because displaying them invites attempts to use them and confuses visitors about what they can do. Hiding alone is not security, as the server must still enforce permissions, but a clean interface that matches permissions is both clearer and safer.

The second principle is to require deliberate, confirmed actions for sensitive changes. Deleting an account, changing a role, removing content, or altering a payment setting should never happen from a single, accidental click on a link. The interface should ask for confirmation, explain the consequence in plain language, and, where appropriate, require re-authentication or an additional step. Confirmation dialogs should be specific: they should name the item or account affected and the action to be taken, rather than using a generic question that a visitor might dismiss without reading. That specificity helps legitimate administrators avoid mistakes, and it makes accidental or tricked actions less likely to succeed unnoticed.

The third principle is to keep admin interfaces separate and unobtrusive. Admin pages should not be linked from public navigation in a way that invites curiosity, and they should not be indexed by search engines. They should use the same accessible, clear design as the rest of the site, because administrators are also users who benefit from clarity, but they should not be discoverable through public browsing. If an admin page is reached by a visitor who is not authorised, the server should return a generic response, such as a not-found or sign-in page, rather than a message confirming that an admin page exists at that address. That generic response avoids leaking information about the site's structure, while still protecting the page.

Finally, design admin and account pages to be auditable. Show administrators what changed, when, and by whom, where the back end provides that information, so that mistakes and misuse can be traced. Log sensitive actions on the server, and display recent activity to the account holder, so they can notice unauthorised changes. Those features are back-end capabilities, but their front-end presentation — a clear activity list, a understandable change history — is what makes them useful to real people. An admin interface that is clear, confirmed, permission-matched, and auditable supports responsible administration, and it demonstrates that security is not just about blocking attackers, but about helping legitimate users act safely.

Check: Why is hiding an admin link not sufficient security on its own?

Section 42 — Accessibility of security messages

Security messages — warnings, errors, confirmations, and permission explanations — must be accessible, because a warning that some visitors cannot perceive or understand does not protect them. Accessibility here means more than meeting a checklist; it means ensuring that every visitor, regardless of ability, device, or assistive technology, can receive, understand, and act on security information. A warning that is conveyed only by colour, only by an icon, or only in a small, low-contrast note will fail for visitors who cannot see that colour, icon, or text. A confirmation that can only be dismissed with a mouse will fail for keyboard users. Security that excludes some visitors is incomplete security.

The first habit is to convey security information through multiple channels. Use clear text, not colour alone, to indicate an error or a warning. Pair an icon with a text label, so the meaning does not depend on recognising the icon. Use headings and structure so that a screen reader user can navigate to the message, and ensure that messages are announced appropriately, rather than appearing silently in a part of the page the user is not focused on. For dynamic messages, such as a form error that appears after submission, ensure the message is placed where assistive technology will encounter it, and that focus moves sensibly, so the visitor is not left wondering why nothing happened. Those practices are standard accessibility, applied to a security context.

The second habit is to write security messages in plain language. A message that says a session has expired and asks the visitor to sign in again is clearer, and therefore safer, than a message full of technical terms that the visitor cannot act on. A permission explanation that says why a camera is needed, in one sentence, helps a visitor decide wisely, while a vague warning that something is dangerous, without saying what or why, encourages either blind acceptance or blind refusal, neither of which is informed consent. Plain language is not dumbing down; it is respecting the visitor's ability to make a good decision when given clear information.

Finally, test security messages with accessibility in mind. Navigate a form using only a keyboard, and confirm that errors are reachable and that focus does not get trapped. Use a screen reader, even briefly, to hear how a warning is announced, and check contrast and text size for warning text on a phone. Involve visitors with different abilities in testing where possible, because they will notice barriers that a checklist misses. A security message that is perceivable, understandable, and actionable by everyone is not just more inclusive; it is more effective, because it protects the whole audience, not only the portion that can see or hear it in one particular way.

Check: Why should a security warning never rely on colour alone?

Section 43 — Security headers in plain language

Security headers are instructions a server sends with a page, telling the browser how to handle that page safely, and they complement the safe display and cookie practices already covered. Like other headers, they are invisible in the page content, but they influence browser behaviour in important ways. A header can tell the browser to always use HTTPS for a site, to refuse to display the page in a frame, to restrict which sources may load resources, or to prevent the browser from guessing a file's type in a way that could be unsafe. Each header addresses a different risk, and together they form a set of browser-enforced rules that support the site's security.

One of the most important headers tells the browser to prefer HTTPS, by instructing it to always use a secure connection for the site in future, even if a visitor types a plain HTTP address or follows an old HTTP link. That header, often called strict transport security, protects against downgrade attacks, where an attacker tries to force a visitor onto an insecure connection. It should be set only when HTTPS is correctly configured for the whole site, including subdomains if they are included, because once a browser learns the rule, it will refuse plain HTTP for that site until the rule expires. That commitment is powerful, but it requires confidence in the site's HTTPS setup, which is why it is usually enabled after HTTPS has been verified to work reliably.

Another header controls framing, by telling the browser whether the page may be displayed in a frame on another site, which relates to the clickjacking defence discussed earlier. A related policy, content security policy, was covered in its own sections, and it is itself delivered as a header. Other headers prevent the browser from guessing content types, which could otherwise cause a file intended as data to be interpreted as a script, and control how much referrer information is sent to other sites when a visitor follows a link, which protects privacy. You do not need to memorise every header's name or syntax to understand their purpose: they are browser instructions that make safe behaviour the default, even if page code is imperfect.

As a front-end developer, your role is to understand what these headers do, to ask for sensible ones on production sites, and to test that they do not break legitimate features. Headers are set by the server or host, so you may need to work with whoever controls hosting to configure them, and to verify them using developer tools, which show the headers received with each response. Avoid copying a long list of headers from a tutorial without understanding each one, because a wrong header can block legitimate content. Learn the few that matter, check them on launch, and review them when features change. Security headers are quiet, effective layers, and they deserve a place in every production checklist.

Check: What does a header that enforces HTTPS protect a visitor from?

Section 44 — Testing your own site defensively

Defensive testing means checking your own site, which you own or have permission to test, for security weaknesses, so you can fix them before anyone else finds them. It is distinct from attacking someone else's site, which is unauthorised and, in many jurisdictions including Nigeria, a crime. Defensive testing is done on your own projects, in a test environment where possible, with the goal of improving safety, not of causing harm or accessing data you are not entitled to. That boundary — only test systems you own or have explicit, written permission to test — is absolute, and it applies to every technique in this chapter, however harmless it may seem.

A practical defensive test starts with a checklist, not with clever tricks. Review every place where user data is displayed, and confirm it is displayed as text, not as HTML, unless a deliberate, reviewed process protects it. Review every form, and confirm it uses the correct method, includes any token the server expects, and shows helpful, non-leaking errors. Review storage use, and confirm that no secrets or sensitive tokens are stored in browser storage. Review cookies, and confirm that session cookies use sensible flags. Review third-party scripts and embeds, and confirm each one is still needed and still trusted. That systematic review, done with a checklist, catches more real problems than any single tool, because it examines design, not just symptoms.

You can also test with unusual but harmless inputs on your own site, such as long names, names containing punctuation and markup-like characters, empty fields, and very large values, to confirm that the site displays them safely and handles them gracefully. The goal of such testing is to observe your own site's behaviour: does it display the input as text, does it show a clear error, does it preserve other data? If the answer to any of those is no, you have found a bug to fix, in your own code, on your own site. Record the test, the result, and the fix, so the same issue does not recur. Testing with your own data, on your own system, is a normal part of development, and it should be done in a test environment first, so that test data does not pollute a live site.

Finally, treat defensive testing as an ongoing habit, not a one-time event. Re-run your checklist when you add a feature, when you update a dependency, and before each major launch. If you find a problem in a system you do not own, do not exploit it or probe it further; report it through the owner's responsible disclosure channel, if one exists, with only the minimum information needed to describe it, and without accessing private data. Defensive testing is about making your own work safer, and about respecting others' systems in the same way you would want yours to be respected.

Check: What is the boundary that makes defensive testing legitimate?

Section 45 — Reporting a vulnerability responsibly

If you discover a security weakness in a system you do not own, the responsible action is to report it through the proper channel, without exploiting it, without accessing private data, and without publicising it before the owner has had a chance to fix it. That process is called responsible disclosure, and it exists because publicising a weakness before it is fixed puts users at risk, while silently ignoring it leaves them at risk as well. Reporting gives the owner a chance to fix the problem, protects users in the meantime, and respects the boundary between testing your own systems and interfering with others'. It is the ethical counterpart to defensive testing, and it applies even when the weakness seems minor.

A responsible report is clear, minimal, and helpful. It describes, in general terms, what kind of weakness you observed and where, without including private data, credentials, or details that would let someone else exploit it. It states how you encountered it, in the course of normal use, without describing attack steps or providing code that could be reused against the system. It suggests, at a high level, what kind of fix might help, such as safer display or a server check, without prescribing an attack. It also gives the owner reasonable time to respond and fix, and it respects their disclosure policy if they have one, including any request not to publicise the issue until it is resolved. A report written in that spirit is more likely to be taken seriously than one that is vague, threatening, or boastful.

Do not confuse responsible disclosure with permission to test. Discovering a weakness during normal use is different from deliberately probing a system you do not own, which may be unauthorised and unlawful, even if your intentions are good. If you did not have permission to test, stop, do not probe further, and report only what you observed in normal use, through the owner's contact or disclosure channel. If no channel exists, a brief, polite message to the site's contact address, describing the issue in general terms, is a reasonable step, but do not demand a reward, do not threaten disclosure, and do not continue to access the system to gather more evidence. The goal is to help users, not to prove a point.

For your own sites, make reporting easy. Provide a contact address or a simple disclosure policy, so that someone who finds a problem can tell you privately, without feeling they must publicise it to be heard. Respond promptly and graciously, even if the report turns out to be a false alarm, because a reporter who is thanked is more likely to report again, while one who is ignored or threatened will go elsewhere. A culture of responsible reporting, on both sides, makes the web safer for everyone, and it starts with treating a report as a gift, not as an accusation.

Check: What should a responsible vulnerability report include, and what should it avoid?

Section 46 — Security checklists for forms

A security checklist for forms turns the principles in this chapter into a repeatable review, so that every form on a site is checked in the same way, regardless of who built it or how rushed the launch was. Checklists work because they externalise memory: instead of relying on a developer to remember every security habit under pressure, the checklist prompts them, item by item, and creates a record that the check was done. For forms, that checklist should cover the form's purpose, its fields, its submission, its messages, and its data handling, in plain language that a reviewer can follow without specialist knowledge.

Start with purpose and fields. Does the form collect only what it needs, and is each field's purpose clear to the visitor? Are labels present for every field, and are required fields marked honestly, rather than marking everything required by default? Are sensitive fields, such as passwords, using the correct input types and autocomplete values? Is there a clear explanation of what will happen after submission, and how long data will be kept? Those questions ensure the form is honest and minimal before any technical check begins, because a form that collects unnecessary data is a privacy problem even if it is implemented securely.

Next, check submission and handling. Does the form submit over HTTPS, to the correct address, using the correct method, with any token the server expects for state-changing actions? Does it validate in the browser for usability, while relying on the server for authoritative checks? Does it handle failures gracefully, showing a clear message and preserving the visitor's other entries, without leaking information about accounts or system internals? Does it avoid storing credentials or sensitive data in browser storage, and does it clear sensitive data on sign-out or expiry? Each of those checks corresponds to a section in this chapter, and together they cover the form's journey from typing to receipt.

Finally, check messages and testing. Are success messages clear and accurate, confirming what was submitted without repeating sensitive data unnecessarily? Are error messages helpful, consistent, and non-leaking? Has the form been tested with ordinary input, with missing fields, with unusually long input, and with a refusal of any permission it requests, on a phone as well as on a desktop? Has it been tested in a private window, to ensure cached data does not hide a problem? A form that passes those checks is not guaranteed to be perfect, but it is far more likely to be safe, usable, and trustworthy than one that was never systematically reviewed. Keep the checklist with the project, and update it when you learn from a mistake. Keep the checklist with the project, and update it when you learn from a mistake.

Check: Why does a checklist help a rushed developer more than memory alone?

Section 47 — Security checklists for displaying data

Displaying data safely is the most frequent security task in front-end work, because almost every page displays something that came from somewhere else, and a checklist for display helps ensure that task is done consistently. The checklist starts with a simple inventory: list every place on the page where data appears, including text, attributes, addresses, and any content loaded from a server, from storage, or from an address. For each place, note where the data came from and whether it could contain untrusted content. That inventory, even when done quickly, often reveals a display point that was overlooked, such as a title derived from a query value or a description loaded from a feed, which is exactly the kind of point where unsafe display creeps in.

For each display point, the checklist asks a short series of questions. Is the data displayed as text, using text content, or as HTML, and if as HTML, what deliberate, reviewed process protects it? If the data appears in an attribute, is it set through a safe method and escaped for that context, with consistent quoting? If it appears in an address, is it encoded for that context and checked against allowed schemes and destinations? If it is rich text, has it passed through a maintained sanitiser with a narrow allowlist, and is that sanitiser kept up to date? If the answer to any of those is unclear, the display point needs attention before launch, because an unclear answer usually means the handling was not deliberate.

The checklist also covers the surrounding protections. Is a sensible content security policy in place, as a second layer, without being relied upon as the only defence? Are third-party scripts on the page still needed and trusted, and do they have access to the displayed data? Are cookies for the page set with sensible flags, so that a display problem, if one ever occurred, would not easily expose a session? Those questions connect display safety to the broader layers discussed in this chapter, and they prevent a narrow focus on one display point from missing a systemic issue, such as a third-party script that could read displayed data.

Finally, the checklist ends with testing. Has the page been tested with data containing punctuation, markup-like characters, and unusually long values, on your own test data, to confirm it displays as text? Has it been tested in a second browser and on a phone, where rendering may differ? Has the test been recorded, so a future editor knows what was checked? A display checklist, used for every new page and every new feature, turns safe display from a principle into a habit, and habits, unlike intentions, survive deadlines. Keep the checklist short enough to actually use, and update it when a new display pattern appears in your projects.

Check: Why should a display review start with an inventory of every place data appears?

Section 48 — Security checklists before launch

A pre-launch security checklist is the final review before a site becomes public, and it exists because a live site faces real visitors, real data, and real attention in a way a private project does not. The checklist should be run on a preview or staging version that mirrors production, so that problems are found before they affect visitors, and it should be signed off, even informally, by the person responsible for the launch. It does not need to be long, but it does need to be used every time, because the items it covers are the ones most often forgotten under launch pressure: a secret left in code, a form that was never tested end to end, a cookie flag that was never set, or a third-party script that was added for testing and never removed.

The checklist covers several areas in sequence. First, code and secrets: search the bundle for secrets, tokens, and passwords, confirm that configuration files with secrets are excluded from version control, and confirm that no test data or private notes are included in the deployed files. Second, display and input: confirm that every display point uses safe methods, that forms use HTTPS and correct methods, that server validation is in place for important rules, and that error messages are helpful and non-leaking. Third, cookies and sessions: confirm that session cookies use sensible flags, that sign-out actually ends the session, and that sensitive actions require confirmation. Fourth, third parties and dependencies: confirm that every script, embed, and library is still needed, trusted, and up to date, and that no test or debug tool remains enabled on the live site.

The checklist also covers privacy and legal readiness, which are security-adjacent but essential for a public site. Confirm that a privacy page accurately describes what data is collected, through forms, analytics, and cookies, and that it is linked from the footer. Confirm that permissions are requested only when needed and explained clearly. Confirm that the site works without granting optional permissions, so visitors are not forced to share data to use basic features. Those checks ensure that the site is not only technically safe, but also honest and respectful in its data practices, which is part of what visitors mean by trust.

Finally, record the checklist result. Note the date, the version checked, any problems found and fixed, and any items deferred with a reason and a follow-up date. That record turns a launch from a vague memory into an accountable step, and it helps future editors understand what was verified and what remains to be done. A pre-launch security checklist is not bureaucracy; it is a final, calm review that protects visitors from the moment a site goes live, and it is a habit that will serve you on every project, from a portfolio to a client site.

Check: Why should a pre-launch security check be run on a staging version, not only on local files?

Section 49 — When something goes wrong: incident habits

Even with careful design, something will eventually go wrong: a form may stop delivering, a page may display incorrectly, a dependency may be found vulnerable, or a visitor may report a problem you did not anticipate. Incident habits are the calm, practised responses that turn such an event from a panic into a manageable process. The first habit is to contain the problem: if a feature is causing harm or leaking data, disable or restrict that feature promptly, even if you do not yet understand the full cause. Containment protects visitors while you investigate, and it is better than leaving a harmful feature live while you search for a perfect fix.

The second habit is to preserve information without making the problem worse. Note the time, the page, the action, and any error message, and save logs or screenshots that may help diagnose the cause, without copying private data unnecessarily. Do not delete logs, do not overwrite the affected version before recording what it was, and do not make multiple simultaneous changes in an attempt to fix it quickly, because those changes will obscure which one actually helped. A single, recorded change at a time, with a verification step after each, is slower in the moment but faster overall, because it produces a clear understanding of the cause.

The third habit is to communicate honestly and proportionately. Tell affected visitors, in plain language, what happened, what you have done to contain it, and what they should do, if anything, such as changing a password or checking an account. Do not minimise a serious problem, do not blame visitors, and do not promise a fix time you cannot keep. If the incident involves personal data, follow the relevant reporting obligations for your situation and seek guidance promptly, rather than deciding alone what to disclose. Honest communication, even when it is uncomfortable, preserves trust far better than silence or spin, because visitors who learn about a problem from you are more likely to believe your fix than those who learn about it elsewhere.

Finally, after the incident is resolved, review it and learn from it. Write a short, factual account of what happened, what caused it, what contained it, and what will change to prevent recurrence, and add a check to your checklists if one would have caught it. Share that learning with your team or, where appropriate, with the community, without including private data or details that would help an attacker. Incidents are unwelcome, but they are also the most effective teachers, because they reveal gaps that checklists and testing missed. A developer who responds calmly, contains promptly, communicates honestly, and learns visibly will be trusted to handle the next incident, too.

Check: Why should you contain a harmful feature first, before fully understanding its cause?

Section 50 — A defender's mindset for everyday work

A defender's mindset is not a set of tricks to be used only on security-sensitive projects; it is a way of approaching everyday front-end work, so that safety becomes a natural part of building, rather than a separate task added at the end. It starts with a question you ask for every feature: what data does this feature handle, where does that data come from, where does it go, and what could go wrong if it is unexpected? That question, asked at design time, is more valuable than any checklist applied afterwards, because it shapes the feature's structure before code is written. A feature designed with its data flows in mind is easier to keep safe than one whose flows are discovered later, under pressure.

The mindset also involves humility about your own code. Assume that you will make mistakes, that input will surprise you, and that a protection you rely on may have a gap. That humility leads to layers: safe display plus a content policy, browser validation plus server validation, cookie flags plus tokens, and checklists plus testing. Layers are not a sign of distrust in your own ability; they are a recognition that complex systems fail in complex ways, and that a second layer catches what the first missed. A developer who builds in layers, and who tests those layers on their own projects, produces sites that are resilient, not just correct in the cases they thought of.

Everyday defender habits are small and repeatable. Review display points when you add a feature, rather than at launch. Search for secrets before you push, rather than after deployment. Test with unusual input on your own test data, rather than only with perfect examples. Update dependencies regularly, rather than when a crisis forces you to. Ask where a token lives, how long a session lasts, and what a form collects, rather than accepting defaults without thought. Those habits take minutes, and they compound: a project built with them from the start requires far less security work at launch than one that tries to add safety afterwards.

Finally, a defender's mindset includes responsibility toward visitors and toward other developers. Write clear messages, respect privacy, report problems responsibly, and share what you learn without sharing what would harm others. Security is, at its heart, a form of care: care for the people who will use what you build, for the data they entrust to it, and for the web as a shared space. That care, expressed in small, daily decisions, is what makes a front-end developer not just a builder of interfaces, but a steward of trust. Carry it into every project, and let it shape not only what you build, but how you build it.

Check: What single question should you ask at design time for every feature?

Practical task

Choose a small site you have built, run the display and form security checklists from this chapter on it, and fix every issue you find on your own test copy first. Record each display point you checked, whether it used text content or a reviewed HTML process, each form you checked, and any secret, token, or unnecessary data collection you removed. Do not test any site you do not own or do not have written permission to test, and do not include any attack code or payload in your notes.

Chapter questions

  1. Explain, in plain language, the difference between displaying data as text content and displaying it as HTML content, and why that choice matters for security.
  2. A site displays a visitor's comment safely, but its session cookie has no HttpOnly, Secure, or SameSite flags. Describe the risks of each missing flag, in concept.
  3. A form changes a user's settings. Explain, at concept level, how a CSRF token, SameSite cookies, and server validation together protect that form.
  4. Why is client-side validation alone insufficient for a rule that limits how many times a visitor may perform an action? What must the server do instead?
  5. You find a possible weakness while using a site you do not own, during normal use. Describe the responsible steps you would take, and what you would not do.