Security Basics
Most JavaScript developers will never write a cryptographic primitive — but every one of us writes the code that decides whether a page renders untrusted text, how cookies are sent, what the browser is allowed to load, and which npm packages get to run. The categories below cover the security bugs you are most likely to ship by accident, and the cheap habits that prevent them.
Cross-site scripting (XSS)
XSS happens when attacker-controlled text ends up being parsed as code by the browser. Once their script is running on your origin, they can read cookies, post as the user, or change the DOM. The defence is brutally simple: treat all user data as data, never as HTML.
output-encoding.js
// DANGEROUS — input like "<img src=x onerror=alert(1)>" runs script
container.innerHTML = userInput;
// SAFE — the same string appears as literal text
container.textContent = userInput;
// SAFE — frameworks like React/Vue escape interpolated values by default
function Comment({ text }) {
return <p>{text}</p>; // automatically encoded
}If you truly must render HTML (a rich-text comment, for example) sanitise it first with a library like DOMPurify — never with hand-rolled regex.
Cross-site request forgery (CSRF)
CSRF tricks a logged-in user's browser into firing a state-changing request the user did not intend — usually via a hidden form or image on a malicious page. The browser cheerfully attaches the session cookie, and your server cannot tell the request from a legitimate one.
Set session cookies with
SameSite=Lax(orStrict) so they are not sent on cross-site POSTs.For sensitive endpoints, also require a CSRF token — a random value the server places in a hidden field or response header, and verifies on submit.
Use
SecureandHttpOnlyon session cookies —HttpOnlymakes them invisible to JavaScript, blunting XSS theft.
csrf-fetch.js
async function postWithToken(url, body) {
const csrf = document.querySelector('meta[name="csrf-token"]').content;
return fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf,
},
body: JSON.stringify(body),
});
}Content Security Policy (CSP)
A CSP header tells the browser which sources of script, style, image and connection it is allowed to load. A good policy turns a successful XSS into a no-op because the injected <script src="evil.com/x.js"> is simply refused.
An example response header
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none';
Start with Content-Security-Policy-Report-Only to collect violations without breaking the site. Tighten over time. Avoid unsafe-inline for scripts — that single token erases most of the protection.
Prototype pollution
JavaScript objects inherit from Object.prototype. If untrusted JSON ever ends up at a path like obj.__proto__.isAdmin, every object in the program suddenly has isAdmin: true. This is prototype pollution, and it has bitten enough libraries to be worth defending against.
proto-pollution.js
// Naive deep-merge — vulnerable
function merge(target, src) {
for (const key in src) {
if (typeof src[key] === 'object') {
target[key] = merge(target[key] || {}, src[key]);
} else {
target[key] = src[key];
}
}
return target;
}
merge({}, JSON.parse('{"__proto__":{"isAdmin":true}}'));
console.log({}.isAdmin); // true — every object is now "admin"Reject keys named
__proto__,prototype, andconstructorwhen merging untrusted input.Use
Object.create(null)for dictionaries you build from external data — they have no prototype to pollute.Prefer
Mapfor arbitrary key/value stores. Map keys never collide with prototype methods.
Supply chain — npm audit and friends
A typical front-end app ships hundreds of transitive dependencies. Each one runs on your build machine and in your bundle. The smallest habits make the biggest difference.
Run
npm audit(orpnpm audit,yarn npm audit) in CI and fail the build on high-severity issues.Pin a lockfile (
package-lock.json,pnpm-lock.yaml) and commit it — reproducible installs are the baseline for every other defence.Use
npm ciin CI rather thannpm install. It refuses to install anything not exactly in the lockfile.Be sceptical of packages with one maintainer, recent name squats, or "postinstall" scripts pulling code from the network.
Enable Dependabot / Renovate so security patches arrive as small, reviewable PRs.
A handful of other quick wins
Never trust
eval,new Function(str), orsetTimeout("code"). They turn data into code — exactly the trick XSS depends on.Open third-party links with
rel="noopener noreferrer"so the new tab cannot navigate your tab viawindow.opener.Serve everything over HTTPS, and use
Strict-Transport-Securityto lock browsers into it.Keep secrets out of client-side bundles — anything shipped to the browser is public, no matter how minified.
Validate input on the server and the client. The client check is UX; the server check is the actual gate.