Security Considerations in HTML
Markup is not where security is enforced, and several markup decisions can undermine it entirely. The ones every front end developer should know.
- Concept
- 1. Never trust anything from the client
- 2. Cross site scripting
- 3. Cross site request forgery
- 4. target="_blank" and rel
- 5. iframes
- Protecting your own pages from being framed
- 6. Subresource integrity
- 7. Content Security Policy
- 8. Forms and credentials
- 9. Never store credentials client side
- 10. Do not leak information
- Important rules
- Common mistakes
- Best practices
- Practice
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
Security is enforced on the server. But a handful of markup decisions can open holes, and a handful of attributes can close them. This note covers what a person writing HTML needs to know.
1. Never trust anything from the client
Every attribute, every value, every validation rule in your markup can be changed in DevTools in seconds.
<!-- the reader can edit this to 1 before submitting -->
<input type="hidden" name="price" value="4999">
<!-- removed in two clicks -->
<input type="number" name="quantity" max="10">
<!-- deleted, and the field becomes editable -->
<input name="account_id" value="1043" readonly>Send identifiers, not values. Let the server look up the price, the limit and the ownership from data it controls.
<!-- safe: the server resolves this to a price it owns -->
<input type="hidden" name="product_id" value="kettle-2l">2. Cross site scripting
The most common web vulnerability. It happens whenever user supplied content is inserted into a page as markup rather than as text.
// dangerous: a script tag in the input becomes a script tag in the page
el.innerHTML = userComment;
// safe: inserted as text, always
el.textContent = userComment;<?php
// dangerous
echo "<p>" . $_GET["name"] . "</p>";
// safe
echo "<p>" . htmlspecialchars($_GET["name"], ENT_QUOTES, "UTF-8") . "</p>";The rule: escape on output, in the context you are outputting into. The escaping needed inside an attribute differs from the escaping needed in text content, which differs again inside a URL or a script.
<!-- an unquoted attribute lets an injected value break out -->
<div class=<?= $userClass ?>>
<!-- quoted, and escaped -->
<div class="<?= htmlspecialchars($userClass, ENT_QUOTES) ?>">Where rich content genuinely must be allowed, sanitise it against a whitelist of permitted elements and attributes rather than trying to strip out dangerous ones. Blacklists are always incomplete.
3. Cross site request forgery
An attacker gets a logged in reader to submit a request they did not intend, from another site.
<form action="/account/delete" method="post">
<input type="hidden" name="csrf_token" value="<?= $token ?>">
<button type="submit">Delete my account</button>
</form>The token is generated by the server, tied to the session, and verified on submission. Its safety comes from the server knowing what it issued - not from the field being hidden.
Two supporting rules:
- State changing actions must be POST, never GET. A destructive action behind a link will eventually be triggered by a crawler, a prefetch or an email scanner.
- Session cookies should carry
SameSite=LaxorStrict.
4. target="_blank" and rel
<a href="https://example.org" target="_blank" rel="noopener noreferrer">External</a>Without noopener, the opened page receives a reference to your window through window.opener and can navigate it elsewhere - a phishing technique known as tabnabbing. Modern browsers imply noopener for target="_blank", but stating it protects older ones and costs nothing.
5. iframes
<!-- untrusted content, tightly restricted -->
<iframe src="/preview" title="Document preview"
sandbox="allow-scripts"></iframe>sandbox blocks scripts, forms, popups and top level navigation, and you grant back only what is needed. Note that allow-scripts together with allow-same-origin on same origin content effectively removes the sandbox, because the framed page can rewrite its own attributes.
Protecting your own pages from being framed
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGINClickjacking is loading your page invisibly over a hostile one and tricking a reader into clicking your buttons. Any page with a login, a payment step or a destructive action should send one of these headers.
6. Subresource integrity
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6..."
crossorigin="anonymous"></script>The browser verifies the downloaded file against the hash and refuses to run anything that does not match. It protects against a compromised CDN serving altered code - which has happened to real, widely used libraries.
7. Content Security Policy
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
frame-ancestors 'self';
base-uri 'self';
form-action 'self'A policy that restricts where resources may be loaded from. It is the strongest single defence against cross site scripting: even if an attacker injects a script tag, the browser refuses to execute it unless the source is permitted.
It has a markup consequence. A strict policy blocks inline scripts and inline event handlers, so onclick attributes stop working. That is a good reason to have been using addEventListener all along.
8. Forms and credentials
<form action="/login" method="post">
<label for="user">Username</label>
<input type="text" id="user" name="user" autocomplete="username">
<label for="pass">Password</label>
<input type="password" id="pass" name="pass" autocomplete="current-password">
<input type="hidden" name="csrf_token" value="...">
<button type="submit">Sign in</button>
</form>- POST, never GET. A GET login puts the password in the URL, in history and in every server log.
- HTTPS, always. Neither method encrypts anything by itself.
- Correct
autocompletetokens, so password managers work properly - a security benefit, not only a convenience. - Do not disable paste on password fields. It discourages password managers and long passwords.
9. Never store credentials client side
// readable by any script on the page, including an injected one
localStorage.setItem("token", jwt);Session credentials belong in a cookie marked HttpOnly, Secure and SameSite, which script cannot read at all.
10. Do not leak information
- Comments in the markup are public. No internal URLs, no notes about known bugs, no credentials.
- Data attributes are public and editable.
- Error messages should not reveal file paths, stack traces or database details.
- Do not list admin paths in
robots.txt; it is a public file.
Important rules
- Client side validation is a convenience, never a control.
- Escape on output, in the correct context.
- Every state changing form needs a CSRF token and POST.
- Everything in the page source is visible and editable.
- HTTPS is what encrypts, not POST.
- A Content Security Policy is the strongest defence against injection.
Common mistakes
- Trusting hidden fields,
readonlyattributes ormaxvalues on the server. - Using
innerHTMLwith user supplied content. - Unquoted attributes holding interpolated values.
- No CSRF token.
- Login forms over GET or over plain HTTP.
- Tokens in
localStorage. - Third party scripts with no integrity hash.
- Internal information in comments.
Best practices
- Validate and authorise everything on the server, again.
- Escape on output with the framework helper for that context.
- Sanitise rich content against a whitelist.
- CSRF tokens on every state changing form.
rel="noopener noreferrer"on external links opening in a new tab.- Send a Content Security Policy and
frame-ancestors. - Use
addEventListener, not inline handlers. - Keep credentials in
HttpOnlycookies.
Practice
- Change a hidden field value in DevTools and submit. What must the server do to be safe?
- Insert a value containing markup with
textContentand then withinnerHTML. - Add a Content Security Policy that blocks inline scripts and fix whatever it breaks.
- Audit a page for external links missing
rel="noopener".