Skip to content

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

Front-end security means not letting attackers run their code in your users' browsers or steal their data. Serve everything over HTTPS, never put untrusted text into the page as HTML (that is how XSS happens), add a Content Security Policy as a safety net, use secure cookies, and never ship secrets in front-end code: anything sent to the browser can be read by anyone.

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

Think of it like this: Think of the notice board in a housing society. Anyone can pin a note. If the secretary reads every note aloud word for word as an order, someone can write "Secretary, please give the master key to the man at the gate." XSS is the same mistake: the site takes text from a user and lets the browser treat it as instructions (code) instead of plain words.
TypeHow the attack text arrivesExample
Stored XSSSaved in the database and shown to every visitorA product review containing <img src=x onerror="steal()">
Reflected XSSTaken from the URL and echoed back in the page/search?q=<script>...</script> shown as "Results for ..."
DOM-based XSSYour own JavaScript copies URL or input data into the page unsafelyel.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.

javascript
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);
textContent by default; DOMPurify only when you truly need user HTML
  • 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.

output
Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' https://images.example.com; object-src 'none'; frame-ancestors 'none'
Only your own scripts; images also from one CDN; no plugins; no other site may put your page in a frame
  • 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

output
Set-Cookie: session=a8f3k2; Path=/; Secure; HttpOnly; SameSite=Lax
A session cookie with all three safety flags
  • 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

Careful: Everything in your front-end code is public. Anyone can open DevTools and read your JavaScript, including "hidden" API keys. Keep secret keys (payments, database, paid AI APIs) on a server. Keep them in a .env file that is listed in .gitignore and never commit it; bots scan GitHub for leaked keys within minutes. Careful: in Vite, any variable starting with VITE_ is copied into the browser code on purpose, so never put a secret there.
  • 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.
Tip: Open DevTools, Network tab, click the first request of your page and read its Response Headers: look for Strict-Transport-Security, Content-Security-Policy and the flags in Set-Cookie. Free header scanners such as Mozilla HTTP Observatory give you a grade and a to-do list.

Watch the lesson

Front-end security: HTTPS, XSS, CSP and secrets · 1/8

HTTPS + HSTS

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)

Task 1 · Code · Javascript

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.

Task 2 · Code · Javascript

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".

Task 3 · Practice

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

Question 1

1.What is the safest way to show a user's comment as plain text?

Question 2

2.Your front-end JavaScript contains the secret key of a payment service. What is true?

Question 3

3.Which cookie flag stops JavaScript (and so an XSS bug) from reading a session cookie?

Question 4

4.What is a good first step when adding a Content Security Policy to an existing site?

Question 5

5.Your API has strict CORS settings. Does that stop an attacker calling it with curl?

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.

← Previous: SEO basics: help people find your pages