JavaScript Security Checklist Cheat Sheet
A pocket checklist for safer JavaScript: treat input as hostile, keep secrets out of code, and spot risky patterns fast. You can also download the information as a printable cheat sheet:
Free Bonus: JavaScript Security Checklist Cheat Sheet
Get the JavaScript Security Checklist Cheat Sheet (PDF), a printable reference to XSS and textContent, eval, prototype pollution, input validation with zod, secrets and env vars, node:crypto, CSP, CORS, and safe child processes:
Want a guided path instead of a reference? Start learning at realjs.com.
Stop XSS
- Cross-site scripting (XSS) runs attacker code in your page
- Insert text with
textContent, not markup - Browser-only: the DOM snippets here can’t run in Node.js
Text, Not HTML
const name = "<img src=x onerror=hack()>";
el.innerHTML = name; // Runs the handler!
el.textContent = name; // Shown as text
Escape Before Building HTML
const esc = (s) => s.replace(
/[&<>"']/g,
(c) => `&#${c.charCodeAt(0)};`,
);
esc(`<b class="x">`);
// "<b class="x">"
Sanitize Untrusted HTML
// Not Baseline yet: feature-check first
if ("setHTML" in Element.prototype) {
el.setHTML(untrustedHtml);
}
// Otherwise use DOMPurify.sanitize()
Avoid eval
evalandnew Functionrun any string as code- Never feed them user input, URLs, or JSON
Parse Data Instead of Running It
eval("1 + 1"); // 2, risky
new Function("return 1")(); // 1, risky
JSON.parse('{"a":1}'); // { a: 1 }
Prototype Pollution
__proto__keys in JSON can reachObject.prototype- A naive recursive merge then adds properties to every object
The Attack
function merge(target, src) {
for (const k in src) {
if (typeof src[k] === "object") {
merge(target[k] ??= {}, src[k]);
} else target[k] = src[k];
}
}
merge({}, JSON.parse(
'{"__proto__":{"admin":true}}',
));
({}).admin; // true
Safer Containers
const safe = Object.create(null);
safe.toString; // undefined
const m = new Map([["__proto__", 1]]);
m.get("__proto__"); // 1
m.get("toString"); // undefined
Check Own Keys Only
const o = { a: 1 };
"toString" in o; // true
Object.hasOwn(o, "toString"); // false
Object.hasOwn(o, "a"); // true
Freeze the Prototype
"use strict";
Object.freeze(Object.prototype);
Object.prototype.x = 1;
// TypeError: Cannot add property x,
// object is not extensible
Validate Input
- Validate at every boundary: requests, files, env vars
- Parse into a trusted shape, then use only that
- Run
npm install zodfor schemas
Parse With a Schema
import { z } from "zod";
const User = z.object({
email: z.email(),
age: z.coerce.number().int().min(0),
});
User.parse({ email: "[email protected]", age: "42" });
// { email: '[email protected]', age: 42 }
Handle Bad Input
const r = User.safeParse({
email: "nope",
age: -1,
});
r.success; // false
r.error.issues.map((i) => i.path[0]);
// [ 'email', 'age' ]
Free Bonus: Download the JavaScript Security Checklist Cheat Sheet PDF and keep the essentials at hand.
Secrets and Env Vars
- Never hardcode keys, and never commit
.env - Anything in browser bundles is public
- A leaked key stays in Git history: rotate it
Load a Local .env File
$ echo "API_KEY=sk-test" > .env
$ echo ".env" >> .gitignore
$ node --env-file=.env app.js
Fail Fast When Missing
const key = process.env.API_KEY;
if (!key) {
throw new Error("API_KEY missing");
}
Crypto
- Never use
Math.random()for tokens - Never roll your own crypto or use MD5 for passwords
- Store the salt with each hash; compare in constant time
- Node’s default scrypt cost (
N=16384) is low: OWASP advisesN=2**17,r=8,p=1(needs a highermaxmem)
Random IDs and Tokens
import { randomUUID, randomBytes }
from "node:crypto";
randomUUID(); // Unique ID
randomBytes(32).toString("base64url");
// Browser: crypto.randomUUID()
Hash Passwords With scrypt
import { scrypt, randomBytes,
timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";
const kdf = promisify(scrypt);
const salt = randomBytes(16);
const hash = await kdf("pw", salt, 64);
const again = await kdf("pw", salt, 64);
timingSafeEqual(hash, again); // true
// Throws if the lengths differ
Run Commands Safely
execruns a shell string, so metacharacters biteexecFilepasses arguments as an array, with no shell
Avoid the Shell
import { execFile, exec }
from "node:child_process";
const arg = "a; echo pwned";
exec(`echo ${arg}`, show);
// "a\npwned\n" (injected!)
execFile("echo", [arg], show);
// "a; echo pwned\n"
Browser Defenses
- Headers are enforced by the browser, not by Node.js
- Test them with DevTools: the Network tab shows each header
- CORS only relaxes browsers; it never blocks other clients
Content Security Policy
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; base-uri 'self'; "
+ "frame-ancestors 'none'",
);
// Blocks inline scripts, other origins,
// and framing by other sites
CORS Allowlist
const ALLOWED = new Set([
"https://app.example.com",
]);
const origin = req.headers.origin;
res.setHeader("Vary", "Origin");
if (ALLOWED.has(origin)) {
res.setHeader(
"Access-Control-Allow-Origin", origin,
);
}
Free Bonus: Download the JavaScript Security Checklist Cheat Sheet PDF and keep the essentials at hand.
Dependencies
- Dependencies run with your privileges: see the RealJS npm supply chain cheat sheet
- Run
npm auditand commit your lockfile
Want to go deeper on JavaScript security?
RealJS teaches the mental models that let you write, read, and review modern JavaScript with confidence, whether you or an AI wrote it.
Learn more at realjs.com 💡
You can download this information as a printable cheat sheet:
Free Bonus: JavaScript Security Checklist Cheat Sheet
Get the JavaScript Security Checklist Cheat Sheet (PDF), a printable reference to XSS and textContent, eval, prototype pollution, input validation with zod, secrets and env vars, node:crypto, CSP, CORS, and safe child processes: