Use parameterized queries with ORMs (Sequelize, TypeORM) or query builders (Knex). NEVER concatenate user input into SQL strings. Unsafe: db.query(SELECT * FROM users WHERE email = '${email}') - attacker sends email = ' OR '1'='1. Safe with Sequelize: User.findOne({where: {email: email}}). Safe with pg: client.query('SELECT * FROM users WHERE email = $1', [email]). Safe with mysql2: connection.query('SELECT * FROM users WHERE email = ?', [email]). Parameterized queries ensure input treated as data, not executable SQL. Also validate input types: if (typeof email !== 'string') return error. Use prepared statements for repeated queries (better performance + security). Applies to all databases: PostgreSQL, MySQL, MongoDB (use Mongoose, not string concatenation).
Node.js OWASP Top10 FAQ & Answers
20 expert Node.js OWASP Top10 answers researched from official documentation. Every answer cites authoritative sources you can verify.
unknown
20 questionsUse bcrypt with 10+ salt rounds for password hashing. Install: npm install bcrypt. Hash password: const bcrypt = require('bcrypt'); const saltRounds = 12; const hash = await bcrypt.hash(password, saltRounds). Verify password: const match = await bcrypt.compare(inputPassword, storedHash). Salt rounds: 10 is minimum (2^10 iterations), 12 recommended for 2024 (4x slower than 10, better security), 14+ for high-security. Each increment doubles hashing time - balances security vs UX. Never use crypto.createHash('sha256') for passwords (no salt, too fast). bcrypt is adaptive - increase rounds as hardware improves. Store hash in database (60 characters), never store plaintext passwords. Automatically generates unique salt per password. NEVER: md5/sha1 (broken), plain crypto.createHash (no salt), custom hashing (use battle-tested library).
4 critical practices: (1) HTTPS only: Enforce TLS 1.2+ with strong ciphers, redirect HTTP to HTTPS. Use Strict-Transport-Security header. (2) Never hardcode secrets: Use environment variables (process.env.SECRET) or secrets managers (AWS Secrets Manager, Azure Key Vault). Add sensitive files to .gitignore. (3) Encrypt data at rest: Use AES-256-GCM for database encryption: crypto.createCipheriv('aes-256-gcm', key, iv). (4) Filter response data: Don't expose password hashes, internal IDs, system paths. Pattern: const sanitize = (user) => ({id: user.id, email: user.email}) - explicit whitelist. Use helmet to remove revealing headers. Configure error handlers to not expose stack traces in production. Scan code for secrets: use git-secrets, truffleHog. Regular security audits.
3-layer defense: (1) Input validation: Validate all user input against expected format. Use validator.js: validator.isEmail(email), reject unexpected patterns. (2) Output encoding: Escape HTML special characters before rendering. Use template engines with auto-escaping (Pug, EJS with <%- for raw, <%= for escaped). Manual: const escape = (str) => str.replace(/[&<>"']/g, (char) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char])). (3) Content-Security-Policy: Use helmet CSP to block inline scripts: helmet.contentSecurityPolicy({directives: {scriptSrc: ["'self'"]}}). Never use dangerouslySetInnerHTML in React without sanitization. Use DOMPurify for user HTML: DOMPurify.sanitize(html). Test with XSS payloads: , <img src=x onerror=alert('XSS')>.
Implement authorization on EVERY protected endpoint, never trust client claims. Pattern: middleware checks if authenticated user can access resource. Code: const authorize = (requiredRole) => (req, res, next) => {if (!req.user) return res.status(401).json({error: 'Unauthorized'}); if (!req.user.roles.includes(requiredRole)) return res.status(403).json({error: 'Forbidden'}); next();}. Use: app.get('/admin/users', authenticate, authorize('admin'), getUsers). Principle of least privilege: Grant minimum permissions needed. Check authorization at data layer too: User.findOne({where: {id, userId: req.user.id}}) - ensures user owns resource. Don't expose internal IDs: Use UUIDs, not sequential integers. Prevent horizontal privilege escalation: User shouldn't access other user's data by changing ID. Test: Try accessing resources as different users, verify 403 Forbidden. Use JWT claims for roles but validate on server.
4 practices: (1) Run npm audit regularly: Checks for known vulnerabilities in dependencies. Run: npm audit, fix with npm audit fix (auto-updates), manually fix breaking changes. Set up: npm audit in CI/CD pipeline, fail build on high/critical vulnerabilities. (2) Use npm outdated: Shows available updates. Update: npm update (respects semver), npm install package@latest for major versions. (3) Use Snyk or Dependabot: Automated vulnerability scanning, creates PRs for fixes. Snyk: snyk test checks vulnerabilities, snyk monitor for continuous monitoring. (4) Pin versions: Use exact versions in package.json (1.2.3 not ^1.2.3) for production, test updates in staging first. Review dependencies before installing: Check last updated, weekly downloads, known vulnerabilities. Remove unused dependencies. Use npm ci in production (installs exact versions from package-lock.json).
Prevent OWASP A08:2021 (Software and Data Integrity Failures, CWE-502) by never accepting serialized objects from untrusted sources. JSON.parse is safe (only creates data, no code execution) but validate structure after parsing. Pattern: const data = JSON.parse(input); const schema = Joi.object({id: Joi.number().required(), name: Joi.string().max(100).required()}); const {error, value} = schema.validate(data); if (error) throw new Error('Invalid data'). NEVER use eval(), Function() constructor, node-serialize, or serialize-javascript with user input. For session storage, use signed cookies with cryptographic integrity checks: app.use(cookieParser(crypto.randomBytes(64).toString('hex'))). Implement digital signatures on serialized data: const hmac = crypto.createHmac('sha256', secret).update(data).digest('hex'). Enforce strict type constraints and isolate deserialization in low-privilege environments. Monitor deserialization errors and log suspicious patterns. Use trusted npm repositories only.
Log 6 security events: (1) Authentication: Login attempts (success/fail), password resets, account lockouts. (2) Authorization: Access denied (403), privilege escalation attempts. (3) Input validation: Rejected inputs, suspicious patterns (SQL injection attempts). (4) Changes: User data modifications, permission changes, configuration updates. (5) Errors: Uncaught exceptions, external service failures, rate limit triggers. (6) System: Application start/stop, dependency changes. Use structured logging: winston, pino. Pattern: logger.warn({event: 'auth.failed', ip: req.ip, email, reason: 'invalid_password'}). DON'T log: Passwords, tokens, credit cards, full request bodies. Sanitize logs: Remove sensitive data before logging. Centralize: Send to ELK, Splunk, Datadog. Alert on: Multiple failed logins (brute force), unusual access patterns, system errors. Retain logs 90+ days for forensics.
Prevent OWASP A10:2021 (SSRF) by validating and restricting all outbound HTTP requests. Attack vector: Attacker tricks server into fetching internal URLs (AWS metadata 169.254.169.254, internal services, bypass firewall). Layered defense: (1) Allowlist domains: const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com']; const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Forbidden host'). (2) Blacklist private IPs: Reject 127.0.0.1, 10.0.0.0/8, 192.168.0.0/16, 169.254.0.0/16 (AWS IMDS), 172.16.0.0/12, ::1. Use library: is-private-ip. (3) Protocol validation: Only allow https://, reject file://, gopher://, ftp://. (4) Cloud metadata protection: Enforce IMDSv2 (require HTTP tokens) on AWS EC2 instances. (5) Network egress controls: Use firewall rules, run in isolated VPC with strict outbound rules. (6) Timeout limits: Prevent hanging on internal endpoints. Never: fetch(userInput) without validation. 2025 best practice: Implement defense-in-depth with allowlists + network controls.
Prevent XML External Entity (XXE) attacks by disabling external entity processing and DTD in XML parsers. XXE attack vector: Attacker sends XML with <!DOCTYPE> and <!ENTITY> declarations that read local files (/etc/passwd), trigger SSRF (http://internal), or cause DoS (billion laughs attack). Node.js has no native XML parser - use third-party libraries securely. Safe configurations: (1) xml2js (safe by default): const parser = new xml2js.Parser(); - external entities disabled by default. (2) fast-xml-parser: const parser = new XMLParser({processEntities: false, allowBooleanAttributes: false, ignoreAttributes: false}). (3) libxmljs (UNSAFE by default): MUST explicitly disable: libxmljs.parseXml(xml, {noent: false, dtdload: false, dtdvalid: false}). 2025 best practice: Prefer JSON over XML (eliminates XXE risk entirely). If XML required: Disable DTD processing, disable external entities, validate against strict XSD schema, sanitize all inputs. Recent 2025 attacks target cloud apps and APIs with insecure XML parsers.
Use parameterized queries with ORMs (Sequelize, TypeORM) or query builders (Knex). NEVER concatenate user input into SQL strings. Unsafe: db.query(SELECT * FROM users WHERE email = '${email}') - attacker sends email = ' OR '1'='1. Safe with Sequelize: User.findOne({where: {email: email}}). Safe with pg: client.query('SELECT * FROM users WHERE email = $1', [email]). Safe with mysql2: connection.query('SELECT * FROM users WHERE email = ?', [email]). Parameterized queries ensure input treated as data, not executable SQL. Also validate input types: if (typeof email !== 'string') return error. Use prepared statements for repeated queries (better performance + security). Applies to all databases: PostgreSQL, MySQL, MongoDB (use Mongoose, not string concatenation).
Use bcrypt with 10+ salt rounds for password hashing. Install: npm install bcrypt. Hash password: const bcrypt = require('bcrypt'); const saltRounds = 12; const hash = await bcrypt.hash(password, saltRounds). Verify password: const match = await bcrypt.compare(inputPassword, storedHash). Salt rounds: 10 is minimum (2^10 iterations), 12 recommended for 2024 (4x slower than 10, better security), 14+ for high-security. Each increment doubles hashing time - balances security vs UX. Never use crypto.createHash('sha256') for passwords (no salt, too fast). bcrypt is adaptive - increase rounds as hardware improves. Store hash in database (60 characters), never store plaintext passwords. Automatically generates unique salt per password. NEVER: md5/sha1 (broken), plain crypto.createHash (no salt), custom hashing (use battle-tested library).
4 critical practices: (1) HTTPS only: Enforce TLS 1.2+ with strong ciphers, redirect HTTP to HTTPS. Use Strict-Transport-Security header. (2) Never hardcode secrets: Use environment variables (process.env.SECRET) or secrets managers (AWS Secrets Manager, Azure Key Vault). Add sensitive files to .gitignore. (3) Encrypt data at rest: Use AES-256-GCM for database encryption: crypto.createCipheriv('aes-256-gcm', key, iv). (4) Filter response data: Don't expose password hashes, internal IDs, system paths. Pattern: const sanitize = (user) => ({id: user.id, email: user.email}) - explicit whitelist. Use helmet to remove revealing headers. Configure error handlers to not expose stack traces in production. Scan code for secrets: use git-secrets, truffleHog. Regular security audits.
3-layer defense: (1) Input validation: Validate all user input against expected format. Use validator.js: validator.isEmail(email), reject unexpected patterns. (2) Output encoding: Escape HTML special characters before rendering. Use template engines with auto-escaping (Pug, EJS with <%- for raw, <%= for escaped). Manual: const escape = (str) => str.replace(/[&<>"']/g, (char) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char])). (3) Content-Security-Policy: Use helmet CSP to block inline scripts: helmet.contentSecurityPolicy({directives: {scriptSrc: ["'self'"]}}). Never use dangerouslySetInnerHTML in React without sanitization. Use DOMPurify for user HTML: DOMPurify.sanitize(html). Test with XSS payloads: , <img src=x onerror=alert('XSS')>.
Implement authorization on EVERY protected endpoint, never trust client claims. Pattern: middleware checks if authenticated user can access resource. Code: const authorize = (requiredRole) => (req, res, next) => {if (!req.user) return res.status(401).json({error: 'Unauthorized'}); if (!req.user.roles.includes(requiredRole)) return res.status(403).json({error: 'Forbidden'}); next();}. Use: app.get('/admin/users', authenticate, authorize('admin'), getUsers). Principle of least privilege: Grant minimum permissions needed. Check authorization at data layer too: User.findOne({where: {id, userId: req.user.id}}) - ensures user owns resource. Don't expose internal IDs: Use UUIDs, not sequential integers. Prevent horizontal privilege escalation: User shouldn't access other user's data by changing ID. Test: Try accessing resources as different users, verify 403 Forbidden. Use JWT claims for roles but validate on server.
4 practices: (1) Run npm audit regularly: Checks for known vulnerabilities in dependencies. Run: npm audit, fix with npm audit fix (auto-updates), manually fix breaking changes. Set up: npm audit in CI/CD pipeline, fail build on high/critical vulnerabilities. (2) Use npm outdated: Shows available updates. Update: npm update (respects semver), npm install package@latest for major versions. (3) Use Snyk or Dependabot: Automated vulnerability scanning, creates PRs for fixes. Snyk: snyk test checks vulnerabilities, snyk monitor for continuous monitoring. (4) Pin versions: Use exact versions in package.json (1.2.3 not ^1.2.3) for production, test updates in staging first. Review dependencies before installing: Check last updated, weekly downloads, known vulnerabilities. Remove unused dependencies. Use npm ci in production (installs exact versions from package-lock.json).
Prevent OWASP A08:2021 (Software and Data Integrity Failures, CWE-502) by never accepting serialized objects from untrusted sources. JSON.parse is safe (only creates data, no code execution) but validate structure after parsing. Pattern: const data = JSON.parse(input); const schema = Joi.object({id: Joi.number().required(), name: Joi.string().max(100).required()}); const {error, value} = schema.validate(data); if (error) throw new Error('Invalid data'). NEVER use eval(), Function() constructor, node-serialize, or serialize-javascript with user input. For session storage, use signed cookies with cryptographic integrity checks: app.use(cookieParser(crypto.randomBytes(64).toString('hex'))). Implement digital signatures on serialized data: const hmac = crypto.createHmac('sha256', secret).update(data).digest('hex'). Enforce strict type constraints and isolate deserialization in low-privilege environments. Monitor deserialization errors and log suspicious patterns. Use trusted npm repositories only.
Log 6 security events: (1) Authentication: Login attempts (success/fail), password resets, account lockouts. (2) Authorization: Access denied (403), privilege escalation attempts. (3) Input validation: Rejected inputs, suspicious patterns (SQL injection attempts). (4) Changes: User data modifications, permission changes, configuration updates. (5) Errors: Uncaught exceptions, external service failures, rate limit triggers. (6) System: Application start/stop, dependency changes. Use structured logging: winston, pino. Pattern: logger.warn({event: 'auth.failed', ip: req.ip, email, reason: 'invalid_password'}). DON'T log: Passwords, tokens, credit cards, full request bodies. Sanitize logs: Remove sensitive data before logging. Centralize: Send to ELK, Splunk, Datadog. Alert on: Multiple failed logins (brute force), unusual access patterns, system errors. Retain logs 90+ days for forensics.
Prevent OWASP A10:2021 (SSRF) by validating and restricting all outbound HTTP requests. Attack vector: Attacker tricks server into fetching internal URLs (AWS metadata 169.254.169.254, internal services, bypass firewall). Layered defense: (1) Allowlist domains: const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com']; const url = new URL(input); if (!ALLOWED_HOSTS.includes(url.hostname)) throw new Error('Forbidden host'). (2) Blacklist private IPs: Reject 127.0.0.1, 10.0.0.0/8, 192.168.0.0/16, 169.254.0.0/16 (AWS IMDS), 172.16.0.0/12, ::1. Use library: is-private-ip. (3) Protocol validation: Only allow https://, reject file://, gopher://, ftp://. (4) Cloud metadata protection: Enforce IMDSv2 (require HTTP tokens) on AWS EC2 instances. (5) Network egress controls: Use firewall rules, run in isolated VPC with strict outbound rules. (6) Timeout limits: Prevent hanging on internal endpoints. Never: fetch(userInput) without validation. 2025 best practice: Implement defense-in-depth with allowlists + network controls.
Prevent XML External Entity (XXE) attacks by disabling external entity processing and DTD in XML parsers. XXE attack vector: Attacker sends XML with <!DOCTYPE> and <!ENTITY> declarations that read local files (/etc/passwd), trigger SSRF (http://internal), or cause DoS (billion laughs attack). Node.js has no native XML parser - use third-party libraries securely. Safe configurations: (1) xml2js (safe by default): const parser = new xml2js.Parser(); - external entities disabled by default. (2) fast-xml-parser: const parser = new XMLParser({processEntities: false, allowBooleanAttributes: false, ignoreAttributes: false}). (3) libxmljs (UNSAFE by default): MUST explicitly disable: libxmljs.parseXml(xml, {noent: false, dtdload: false, dtdvalid: false}). 2025 best practice: Prefer JSON over XML (eliminates XXE risk entirely). If XML required: Disable DTD processing, disable external entities, validate against strict XSD schema, sanitize all inputs. Recent 2025 attacks target cloud apps and APIs with insecure XML parsers.