I've interviewed 50+ backend and full-stack engineers. Security questions show up in almost every loop now — even for roles that aren't labeled "security." Here are the 13 questions that come up the most, each with the answer in one breath, the bad-vs-good code, the CWE, and — the part most cheat-sheets skip — the ESLint rule that enforces it so you never ship the bad version.
The best answer to "how do you stay current?" isn't "I read CVEs." It's "I enforce these in CI." This is how.
db.query(`SELECT * FROM users WHERE id = ${userId}`); // ❌
db.query("SELECT * FROM users WHERE id = $1", [userId]); // ✅
Say: "Parameterized queries separate data from code." CWE-89. Enforced by
pg/no-unsafe-query.
Stored (saved to the DB), reflected (echoed from the URL), and DOM (written by client JS). The browser-side fix is the same reflex:
element.innerHTML = userInput; // ❌
element.textContent = userInput; // ✅
CWE-79. Enforced by browser-security/no-innerhtml.
crypto.createHash("md5").update(password); // ❌
await bcrypt.hash(password, 12); // ✅
Say: "bcrypt or argon2, per-user salt, a work factor." CWE-916. The weak
hash is caught by node-security/no-weak-hash-algorithm.
Cross-Site Request Forgery rides an authenticated user's cookies to perform
actions they didn't intend. Prevention: synchronizer tokens, SameSite
cookies, origin checks. CWE-352. Enforced by
express-security/require-csrf-protection.
Browsers isolate by origin (scheme + host + port). The controlled relaxations are
CORS, postMessage, and (legacy) JSONP — and an over-broad CORS policy
re-opens everything (CWE-942). browser-security/no-permissive-cors catches
the wildcard.
if (userToken === secretToken) {
} // ❌ leaks via comparison time
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); // ✅ constant-time
CWE-208. Enforced by node-security/no-timing-unsafe-compare.
Verify the signature, check exp, never accept algorithm: "none", and store in
an httpOnly cookie — not localStorage. CWE-347 for the none bypass
(jwt/no-algorithm-none); the storage mistake is browser-security/no-jwt-in-storage.
obj[key] = value; // ❌ key="__proto__" pollutes Object.prototype
if (key !== "__proto__" && key !== "constructor" && key !== "prototype")
obj[key] = value; // ✅
CWE-1321. Caught by secure-coding/detect-object-injection (the rule's own
finding tags CWE-915; CWE-1321 is the canonical JS-prototype-pollution entry).
An HTTP header that restricts what can load: default-src 'self'; script-src 'self' 'nonce-…'.
CWE-693. browser-security/require-csp-headers flags its absence;
no-unsafe-inline-csp flags the 'unsafe-inline' that defeats it.
/^(a+)+$/.test("aaaaaaaaaaaaaaaaaaaaaaaa!"); // ❌ catastrophic backtracking
Regular-Expression Denial of Service. Caught by
secure-coding/no-redos-vulnerable-regex.
- Password hashing (bcrypt/argon2)
- Rate limiting on login + account lockout after repeated failures
- MFA
- Secure session management
- Password reset via time-limited tokens
Most of this is architectural, but the enforceable slice is real: the rate-limit
gap on the login route is express-security/require-rate-limiting (CWE-307).
Environment variables at minimum; a secrets manager (Vault, AWS Secrets Manager)
in production; nothing in code or git history; a rotation policy. CWE-798 —
secure-coding/no-hardcoded-credentials is the backstop for the last one.
AuthN (JWT/OAuth2), AuthZ (RBAC/ABAC), input validation, rate limiting, HTTPS
only, and a tight CORS policy — express-security/require-helmet plus the rate
and CORS rules above cover the configuration half.
| Vulnerability | Prevention | CWE | Enforced by |
|---|---|---|---|
| SQL Injection | Parameterized queries | CWE-89 | pg/no-unsafe-query |
| XSS | Output encoding | CWE-79 | browser-security/no-innerhtml |
| CSRF | Tokens + SameSite | CWE-352 | express-security/require-csrf-protection |
| Weak password hash | bcrypt / argon2 | CWE-916 | node-security/no-weak-hash-algorithm |
| Prototype poll. | Key allow-list | CWE-1321 | secure-coding/detect-object-injection |
| ReDoS | Linear-time regex | CWE-1333 | secure-coding/no-redos-vulnerable-regex |
Reciting these in an interview proves you know them. Wiring them into CI proves
you'll never ship them. Each concept above maps to a rule in a domain-specific
Interlace plugin — install the layers your
stack uses and the bad version gets flagged (run with --max-warnings 0 in CI so
every finding blocks, not just the error-tier ones):
# npm (yarn/pnpm/bun: same packages, that manager's -D/--dev flag)
npm install --save-dev eslint-plugin-secure-coding eslint-plugin-node-security \
eslint-plugin-jwt eslint-plugin-pg eslint-plugin-browser-security eslint-plugin-express-security
// eslint.config.mjs — `configs` is a NAMED export (default export is the plugin)
import { configs as secureCoding } from "eslint-plugin-secure-coding";
import { configs as nodeSecurity } from "eslint-plugin-node-security";
export default [secureCoding.recommended, nodeSecurity.recommended];
| Surface | Support |
|---|---|
| Package managers | npm, yarn, pnpm, bun |
| Node | >= 18.0.0 |
| ESLint | ^8.0.0 || ^9.0.0 || ^10.0.0, flat config |
| Module system | CommonJS — eslint.config.js or .mjs |
| Oxlint | flagship rules wired via the interlace-* ports, CI-gated |
For the full OWASP picture (and the two categories static analysis honestly can't reach), see the OWASP Top 10 mapping.
- 📦 eslint-plugin-secure-coding · node-security · jwt · pg · browser-security
- 📖 Full rule docs (per-rule CWE)
- 💻 Source on GitHub
⭐ Star on GitHub if you'd rather enforce this list than memorize it for the next interview.
I'm Ofri Peretz, a security engineering leader and the author of the Interlace ESLint ecosystem — domain-specific static analysis for security, reliability, and performance on the Node.js stack.