Expert · Fast, accessible, findable and safe: quality for real sites · topic 49 of 64 · 40 min
Front-end security: HTTPS, XSS, CSP and secrets
Explanation
HTTPS everywhere
HTTPS encrypts the traffic between the browser and the server, so nobody on the same cafe Wi-Fi or on the network in between can read or change it. The padlock means the connection is encrypted and the certificate matches the domain; it does not mean the site is honest. Free certificates (Let's Encrypt) and modern hosts make HTTPS automatic, and many browser features (service workers, location, camera) work only on HTTPS. The Strict-Transport-Security (HSTS) header tells browsers to always use HTTPS for your domain, even when someone types http://.
XSS: cross-site scripting
| Type | How the attack text arrives | Example |
|---|---|---|
| Stored XSS | Saved in the database and shown to every visitor | A product review containing <img src=x onerror="steal()"> |
| Reflected XSS | Taken from the URL and echoed back in the page | /search?q=<script>...</script> shown as "Results for ..." |
| DOM-based XSS | Your own JavaScript copies URL or input data into the page unsafely | el.innerHTML = location.hash.slice(1) |
If attack code runs on your page, it can do anything your page can do: read what users type, send requests as them, change the prices they see, or show a fake login form. In intermediate-3 you saw why innerHTML with user text is dangerous; here is the full defence.
const comment = 'Nice! <img src=x onerror="alert(1)">';
// Unsafe: the browser parses it as HTML and runs onerror
// box.innerHTML = comment;
// Safe: shown as plain text
box.textContent = comment;
// Need some user HTML (bold, links)? Sanitise it with a tested library
import DOMPurify from 'dompurify';
box.innerHTML = DOMPurify.sanitize(userHtml);- Never pass user data to eval(), new Function(), setTimeout with a string, or document.write().
- Check URLs before putting them in href or src: "javascript:alert(1)" is a link that runs code. Allow only http: and https:.
- Frameworks like React escape text for you, but their escape hatches (dangerouslySetInnerHTML, v-html) bring the danger back.
- The server must also validate and escape data: front-end checks can always be skipped.
Content Security Policy (CSP)
CSP is an HTTP response header that tells the browser which sources of scripts, styles, images and frames are allowed. If an attacker manages to inject a <script>, a strict CSP stops it from running. It is a safety net, not a replacement for escaping.
Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' https://images.example.com; object-src 'none'; frame-ancestors 'none'- script-src 'self' blocks inline <script> blocks and onclick="..." attributes, so keep JavaScript in .js files (or use nonces, which some frameworks add for you).
- Start with the Content-Security-Policy-Report-Only header: the browser only reports what it would block, so nothing breaks while you tune the policy. Then switch to the real header.
- frame-ancestors 'none' (or 'self') stops clickjacking: an evil site loads your page in an invisible frame and tricks users into clicking your buttons. The older X-Frame-Options: DENY header does a similar job. frame-ancestors does not work in a <meta> tag; send it as a header.
Cookies, CSRF and CORS
Set-Cookie: session=a8f3k2; Path=/; Secure; HttpOnly; SameSite=Lax- HttpOnly: JavaScript cannot read the cookie, so an XSS bug cannot steal the session.
- Secure: the cookie is sent only over HTTPS.
- SameSite=Lax (or Strict): the cookie is not sent on most requests started by other sites, which blocks most CSRF.
- CSRF (cross-site request forgery): an evil page makes your logged-in browser send a request (like "change my email") to a real site, and the browser attaches your cookie. Defences: SameSite cookies, CSRF tokens in forms, and never changing data with GET requests.
- CORS is a browser rule that lets a server allow other websites to read its responses. It does not protect your server: curl, Postman and scripts ignore CORS completely. Your API still needs real login and permission checks.
Secrets and third-party code
- Each npm package and each <script> from another site runs with full power on your page. Use fewer, well-known ones.
- Commit package-lock.json and install with npm ci, so everyone gets exactly the same versions. Run npm audit to find packages with known security problems, and update them.
- For a script from a CDN, add an integrity="sha384-..." attribute (Subresource Integrity), so the browser refuses a file that was changed.
- Links with target="_blank": modern browsers already treat them as rel="noopener", so the new tab cannot control your page. Adding rel="noopener noreferrer" is still common for old browsers; noreferrer also hides your page address from the other site.
Watch the lesson
Front-end security: HTTPS, XSS, CSP and secrets · 1/8
Serve everything over HTTPS. The padlock means the connection is encrypted, not that the site is honest. HSTS makes browsers always use HTTPS.
Prefer a video by a teacher? Find free videos on “frontend web security xss csp cookies explained” on YouTube ↗
In real life: A college fest registration site
A college fest site lets teams register a team name, which is shown on a public leaderboard with innerHTML. A student registers the team name <img src=x onerror="..."> and the script runs for every visitor (stored XSS). The fix: show names with textContent, add a CSP with script-src 'self', set the admin session cookie to HttpOnly, Secure and SameSite=Lax, and move the payment gateway secret key out of the front-end code into a serverless function. The organisers also rotate the old key, because it was in the Git history.
Practice (0/3 done)
Users can add a website link to their profile, and you show it as <a href="...">. A link like "javascript:alert(1)" would run code when clicked. Write isSafeUrl(url) that returns true only when url is an absolute URL whose protocol is "http:" or "https:", and false for anything else, including text that is not a valid URL. Use new URL(url) inside try/catch (it throws for invalid URLs) and check its .protocol. Example: isSafeUrl("https://example.com") is true; isSafeUrl("javascript:alert(1)") is false.
Write buildCsp(directives) that turns an object into a Content-Security-Policy header value. Each key is a directive name and its value is an array of sources. Write each directive as "name source1 source2", and join the directives with "; " (semicolon and space), in the order of the object. If a directive has an empty array, write just its name. Example: buildCsp({ 'default-src': ["'self'"], 'img-src': ["'self'", 'https://img.example.com'] }) returns "default-src 'self'; img-src 'self' https://img.example.com".
Do a small security check of one of your own projects (its GitHub repo and its live site). Write down what you found and what you fixed.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.