When building modern applications with Node.js, security is not optional โ itโs essential. As your APIs and microservices grow, so does the attack surface. A small misconfiguration or missing validation can expose sensitive data, invite brute-force attacks, or even bring down your entire system.
In this blog, weโll walk through the most important Node.js security best practices with practical examples you can apply right away.
- Validate and Sanitize Input
Never trust user input. Attackers often try SQL Injection, NoSQL Injection, or XSS using cleverly crafted payloads.
import validator from "validator";
app.post("/register", (req, res) => {
const { email } = req.body;
if (!validator.isEmail(email)) {
return res.status(400).send("Invalid email format");
}
res.send("User registered");
});
๐ Always validate data before processing.
- Secure HTTP Headers with Helmet
By default, Node apps donโt include security headers. Use Helmet to add protections like Content-Security-Policy, XSS-Protection, and Strict-Transport-Security.
import helmet from "helmet";
app.use(helmet());
- Prevent NoSQL Injection
If youโre using MongoDB, attackers can inject queries like { "$gt": "" } to bypass filters.
import mongoSanitize from "express-mongo-sanitize";
app.use(mongoSanitize());
- Apply Rate Limiting (Stop Brute-Force)
Attackers often hammer login endpoints. Prevent this with rate limiting.
import rateLimit from "express-rate-limit";
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use("/api/", limiter);
- Enforce HTTPS
Always redirect traffic to HTTPS to protect data in transit.
app.use((req, res, next) => {
if (req.headers["x-forwarded-proto"] !== "https") {
return res.redirect("https://" + req.headers.host + req.url);
}
next();
});
- Manage Secrets Safely
Never hardcode secrets in your codebase. Use environment variables or secret managers.
import dotenv from "dotenv";
dotenv.config();
const jwtSecret = process.env.JWT_SECRET;
- Hash Passwords Before Storing
Storing plain-text passwords is a critical mistake. Always hash them with bcrypt.
import bcrypt from "bcrypt";
const hashPassword = async (password) => {
const salt = await bcrypt.genSalt(12);
return await bcrypt.hash(password, salt);
};
- Update Dependencies Regularly
Outdated packages may contain vulnerabilities. Run:
npm audit fix
and consider tools like Snyk for continuous monitoring.
- Configure CORS Properly
Allow requests only from trusted origins.
import cors from "cors";
app.use(cors({
origin: ["https://myapp.com"],
methods: ["GET", "POST"]
}));
- Protect Against CSRF Attacks
For state-changing requests, implement CSRF protection.
import csurf from "csurf";
app.use(csurf());
- Donโt Leak Error Details
Verbose error messages can expose internal logic. Show generic messages in production.
app.use((err, req, res, next) => {
console.error(err.stack); // log internally
res.status(500).send("Something went wrong!");
});
- Run Node with Least Privileges
Avoid running Node apps as root. Use Docker non-root users or pm2 with restricted permissions.
- Content Security Policy (CSP)
CSP helps control what external scripts or styles your app can load โ mitigating XSS risks.
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trustedscripts.com"]
}
})
);
โ Quick Security Checklist
Validate and sanitize input
Secure headers with Helmet
Prevent NoSQL/SQL injections
Apply rate limiting
Enforce HTTPS
Securely store secrets
Hash passwords with bcrypt
Enable CORS and CSRF protection
Donโt leak stack traces in production
Run apps with least privileges
Keep dependencies updated
๐ Final Thoughts
Securing a Node.js application is an ongoing process, not a one-time task. Start by applying these best practices, then continuously monitor your app for new threats. Remember: a single weak point can compromise the entire system.
By following these steps, youโll build robust, production-ready Node.js applications that can stand against real-world attacks.
Top comments (0)