Security Things That Matter Security is dull & boring until you get hacked. Then it's REALLY interesting. Node is great at making it easy to create APIs overnight, but that also makes it easy to do it wrong. I have seen others get compromised because of: They employed packages they never audited (npm is wonderful & frightening) They rely on input from users (never, EVER do that) They hard-code credentials directly into the code (why in the world?) I am sure you have wondered what you'd do if someone hacked your app. Let’s look at some suggestions to avoid getting hacked. The Basics Check What Users/Customers Send You Always expect users to attempt strange things. Check ALL OF IT. // Bad code app.post('/users', (req, res) => { db.users.create(req.body); // Accepting whatever users send? Bad idea! }); // Better approach app.post('/users', (req, res) => { // Use something like Joi or express-validator if (!req.body.email || !req.body.email.includes('@')) { return res.status(400).send('Invalid email'); } if (typeof req.body.age !== 'number') { return res.status(400).send('Age must be a number'); } // Now it's safer to save db.users.create(req.body); }); I found this out for myself when someone crashed my application by placing an emoji within the username field. Fun times. Login Stuff: Don't Mess This Up JWT tokens are cool but simple to get wrong. Here's my take: // Creating tokens - keep them short lived! const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' } // Dont make these last forever ); // Check tokens on protected routes function checkAuth(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).send('Login required'); } try { const user = jwt.verify(token, process.env.JWT_SECRET); req.user = user; // Add user info to request next(); } catch (err) { return res.status(403).send('Invalid or expired token'); } } // Use it to protect routes app.get('/profile', checkAuth, (req, res) => { // Only logged in users get here }); Auth has two halves: ensuring that the user is who they say they are (authentication) and ensuring that they can do what they're trying to do (authorization). Never, under any circumstances, commit your database password to GitHub // NO NO NO - don't hardcode passwords!! const db = mysql.connect({ host: 'mydatabase.server.com', user: 'admin', password: 'SuperSecret123!' // This should never be in your code }); // Do this instead require('dotenv').config(); const db = mysql.connect({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD }); And DO NOT forget to place your .env file to .gitignore. Real Security Problems I've Seen SQL Injection Still Works?! Yes, it does. And it is so easy to stop: // Dangerous - allows SQL injection app.get('/users', (req, res) => { const name = req.query.name; db.query(`SELECT * FROM users WHERE name = '${name}'`, // BAD! (err, results) => res.json(results) ); }); // Safe - use parameters app.get('/users', (req, res) => { const name = req.query.name; db.query('SELECT * FROM users WHERE name = ?', [name], // This prevents SQL injection (err, results) => res.json(results) ); }); When someone attempts ?name=x'; DROP TABLE users; -- you will be happy you utilized parameters. Too Many Requests = Crashed Server The app crashed when a user abused the search API too much. Implement rate limiting: const rateLimit = require('express-rate-limit'); // Basic protection for all routes const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit per IP }); app.use(limiter); // Extra protection for login attempts const loginLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5 // 5 login attempts per hour }); app.use('/login', loginLimiter); Old Packages = Security Holes Most hacks happen through outdated packages. Check yours: # Run this often! npm audit # Fix what you can npm audit fix Use Helmet for HTTP Headers One line of code that fixes several issues: const helmet = require('helmet'); app.use(helmet()); // Adds security headers FAQs that every Developer should know Q: What do you need to fix first that is most important? A: Input validation. Most attacks start there. Q: What is the best way to know if my API security is sufficient? A: Have someone attempt to break it. Or you can try a tool like OWASP ZAP. Q: How can I prevent security vulnerabilities caused by dependencies?A: Schedule a calendar event to update your dependencies in a timely manner. To detect these vulnerabilities, consider using tools like npm audit, snyk, or dependable. Q: What is the best way to know if my API security is sufficient? A: Have someone attempt to break it. Or you can try a tool like OWASP ZAP. Final Thoughts - Lessons to learn from Security is not an afterthought - it should be built into the code from the outset. Start with these basics: Authenticate all users Secure your auth routes Store Secrets in Environment Variables Update dependencies Set rate limits Always use HTTPS So, which gap will you be closing today? Security Things That Matter Security Things That Matter Security is dull & boring until you get hacked. Then it's REALLY interesting. Node is great at making it easy to create APIs overnight, but that also makes it easy to do it wrong. I have seen others get compromised because of: They employed packages they never audited (npm is wonderful & frightening) They rely on input from users (never, EVER do that) They hard-code credentials directly into the code (why in the world?) They employed packages they never audited (npm is wonderful & frightening) They rely on input from users (never, EVER do that) They hard-code credentials directly into the code (why in the world?) I am sure you have wondered what you'd do if someone hacked your app. Let’s look at some suggestions to avoid getting hacked. The Basics The Basics Check What Users/Customers Send You Always expect users to attempt strange things. Check ALL OF IT. // Bad code app.post('/users', (req, res) => { db.users.create(req.body); // Accepting whatever users send? Bad idea! }); // Better approach app.post('/users', (req, res) => { // Use something like Joi or express-validator if (!req.body.email || !req.body.email.includes('@')) { return res.status(400).send('Invalid email'); } if (typeof req.body.age !== 'number') { return res.status(400).send('Age must be a number'); } // Now it's safer to save db.users.create(req.body); }); Check What Users/Customers Send You Always expect users to attempt strange things. Check ALL OF IT. // Bad code app.post('/users', (req, res) => { db.users.create(req.body); // Accepting whatever users send? Bad idea! }); // Better approach app.post('/users', (req, res) => { // Use something like Joi or express-validator if (!req.body.email || !req.body.email.includes('@')) { return res.status(400).send('Invalid email'); } if (typeof req.body.age !== 'number') { return res.status(400).send('Age must be a number'); } // Now it's safer to save db.users.create(req.body); }); Check What Users/Customers Send You Check What Users/Customers Send You Always expect users to attempt strange things. Check ALL OF IT. // Bad code app.post('/users', (req, res) => { db.users.create(req.body); // Accepting whatever users send? Bad idea! }); // Better approach app.post('/users', (req, res) => { // Use something like Joi or express-validator if (!req.body.email || !req.body.email.includes('@')) { return res.status(400).send('Invalid email'); } if (typeof req.body.age !== 'number') { return res.status(400).send('Age must be a number'); } // Now it's safer to save db.users.create(req.body); }); // Bad code app.post('/users', (req, res) => { db.users.create(req.body); // Accepting whatever users send? Bad idea! }); // Better approach app.post('/users', (req, res) => { // Use something like Joi or express-validator if (!req.body.email || !req.body.email.includes('@')) { return res.status(400).send('Invalid email'); } if (typeof req.body.age !== 'number') { return res.status(400).send('Age must be a number'); } // Now it's safer to save db.users.create(req.body); }); I found this out for myself when someone crashed my application by placing an emoji within the username field. Fun times. Login Stuff: Don't Mess This Up JWT tokens are cool but simple to get wrong. Here's my take: // Creating tokens - keep them short lived! const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' } // Dont make these last forever ); // Check tokens on protected routes function checkAuth(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).send('Login required'); } try { const user = jwt.verify(token, process.env.JWT_SECRET); req.user = user; // Add user info to request next(); } catch (err) { return res.status(403).send('Invalid or expired token'); } } // Use it to protect routes app.get('/profile', checkAuth, (req, res) => { // Only logged in users get here }); Login Stuff: Don't Mess This Up JWT tokens are cool but simple to get wrong. Here's my take: // Creating tokens - keep them short lived! const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' } // Dont make these last forever ); // Check tokens on protected routes function checkAuth(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).send('Login required'); } try { const user = jwt.verify(token, process.env.JWT_SECRET); req.user = user; // Add user info to request next(); } catch (err) { return res.status(403).send('Invalid or expired token'); } } // Use it to protect routes app.get('/profile', checkAuth, (req, res) => { // Only logged in users get here }); Login Stuff: Don't Mess This Up Login Stuff: Don't Mess This Up JWT tokens are cool but simple to get wrong. Here's my take: // Creating tokens - keep them short lived! const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' } // Dont make these last forever ); // Check tokens on protected routes function checkAuth(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).send('Login required'); } try { const user = jwt.verify(token, process.env.JWT_SECRET); req.user = user; // Add user info to request next(); } catch (err) { return res.status(403).send('Invalid or expired token'); } } // Use it to protect routes app.get('/profile', checkAuth, (req, res) => { // Only logged in users get here }); // Creating tokens - keep them short lived! const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' } // Dont make these last forever ); // Check tokens on protected routes function checkAuth(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).send('Login required'); } try { const user = jwt.verify(token, process.env.JWT_SECRET); req.user = user; // Add user info to request next(); } catch (err) { return res.status(403).send('Invalid or expired token'); } } // Use it to protect routes app.get('/profile', checkAuth, (req, res) => { // Only logged in users get here }); Auth has two halves: ensuring that the user is who they say they are (authentication) and ensuring that they can do what they're trying to do (authorization). Never, under any circumstances, commit your database password to GitHub // NO NO NO - don't hardcode passwords!! const db = mysql.connect({ host: 'mydatabase.server.com', user: 'admin', password: 'SuperSecret123!' // This should never be in your code }); // Do this instead require('dotenv').config(); const db = mysql.connect({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD }); Never, under any circumstances, commit your database password to GitHub // NO NO NO - don't hardcode passwords!! const db = mysql.connect({ host: 'mydatabase.server.com', user: 'admin', password: 'SuperSecret123!' // This should never be in your code }); // Do this instead require('dotenv').config(); const db = mysql.connect({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD }); Never, under any circumstances, commit your database password to GitHub Never, under any circumstances, commit your database password to GitHub // NO NO NO - don't hardcode passwords!! const db = mysql.connect({ host: 'mydatabase.server.com', user: 'admin', password: 'SuperSecret123!' // This should never be in your code }); // Do this instead require('dotenv').config(); const db = mysql.connect({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD }); // NO NO NO - don't hardcode passwords!! const db = mysql.connect({ host: 'mydatabase.server.com', user: 'admin', password: 'SuperSecret123!' // This should never be in your code }); // Do this instead require('dotenv').config(); const db = mysql.connect({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD }); And DO NOT forget to place your .env file to .gitignore. Real Security Problems I've Seen Real Security Problems I've Seen SQL Injection Still Works?! Yes, it does. And it is so easy to stop: // Dangerous - allows SQL injection app.get('/users', (req, res) => { const name = req.query.name; db.query(`SELECT * FROM users WHERE name = '${name}'`, // BAD! (err, results) => res.json(results) ); }); // Safe - use parameters app.get('/users', (req, res) => { const name = req.query.name; db.query('SELECT * FROM users WHERE name = ?', [name], // This prevents SQL injection (err, results) => res.json(results) ); }); SQL Injection Still Works?! Yes, it does. And it is so easy to stop: // Dangerous - allows SQL injection app.get('/users', (req, res) => { const name = req.query.name; db.query(`SELECT * FROM users WHERE name = '${name}'`, // BAD! (err, results) => res.json(results) ); }); // Safe - use parameters app.get('/users', (req, res) => { const name = req.query.name; db.query('SELECT * FROM users WHERE name = ?', [name], // This prevents SQL injection (err, results) => res.json(results) ); }); SQL Injection Still Works?! Yes, it does. And it is so easy to stop: // Dangerous - allows SQL injection app.get('/users', (req, res) => { const name = req.query.name; db.query(`SELECT * FROM users WHERE name = '${name}'`, // BAD! (err, results) => res.json(results) ); }); // Safe - use parameters app.get('/users', (req, res) => { const name = req.query.name; db.query('SELECT * FROM users WHERE name = ?', [name], // This prevents SQL injection (err, results) => res.json(results) ); }); // Dangerous - allows SQL injection app.get('/users', (req, res) => { const name = req.query.name; db.query(`SELECT * FROM users WHERE name = '${name}'`, // BAD! (err, results) => res.json(results) ); }); // Safe - use parameters app.get('/users', (req, res) => { const name = req.query.name; db.query('SELECT * FROM users WHERE name = ?', [name], // This prevents SQL injection (err, results) => res.json(results) ); }); When someone attempts ?name=x'; DROP TABLE users; -- you will be happy you utilized parameters. Too Many Requests = Crashed Server The app crashed when a user abused the search API too much. Implement rate limiting: Too Many Requests = Crashed Server The app crashed when a user abused the search API too much. Implement rate limiting: Too Many Requests = Crashed Server Too Many Requests = Crashed Server The app crashed when a user abused the search API too much. Implement rate limiting: const rateLimit = require('express-rate-limit'); // Basic protection for all routes const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit per IP }); app.use(limiter); // Extra protection for login attempts const loginLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5 // 5 login attempts per hour }); app.use('/login', loginLimiter); const rateLimit = require('express-rate-limit'); // Basic protection for all routes const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100 // limit per IP }); app.use(limiter); // Extra protection for login attempts const loginLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5 // 5 login attempts per hour }); app.use('/login', loginLimiter); Old Packages = Security Holes Most hacks happen through outdated packages. Check yours: # Run this often! npm audit # Fix what you can npm audit fix Old Packages = Security Holes Most hacks happen through outdated packages. Check yours: # Run this often! npm audit # Fix what you can npm audit fix Old Packages = Security Holes Old Packages = Security Holes Most hacks happen through outdated packages. Check yours: # Run this often! npm audit # Fix what you can npm audit fix # Run this often! npm audit # Fix what you can npm audit fix Use Helmet for HTTP Headers Use Helmet for HTTP Headers One line of code that fixes several issues: const helmet = require('helmet'); app.use(helmet()); // Adds security headers const helmet = require('helmet'); app.use(helmet()); // Adds security headers FAQs that every Developer should know FAQs that every Developer should know Q: What do you need to fix first that is most important? A: Input validation. Most attacks start there. Q: What is the best way to know if my API security is sufficient? A: Have someone attempt to break it. Or you can try a tool like OWASP ZAP. Q: How can I prevent security vulnerabilities caused by dependencies? A: Schedule a calendar event to update your dependencies in a timely manner. To detect these vulnerabilities, consider using tools like npm audit, snyk, or dependable. Q: What is the best way to know if my API security is sufficient? A: Have someone attempt to break it. Or you can try a tool like OWASP ZAP. Final Thoughts - Lessons to learn from Final Thoughts - Lessons to learn from Security is not an afterthought - it should be built into the code from the outset. Start with these basics: Authenticate all users Secure your auth routes Store Secrets in Environment Variables Update dependencies Set rate limits Always use HTTPS Authenticate all users Secure your auth routes Store Secrets in Environment Variables Update dependencies Set rate limits Always use HTTPS So, which gap will you be closing today?