Security
Follow secure coding practices to protect applications from common vulnerabilities and attacks.
This includes input validation, authentication, access control, safe use of third-party dependencies, and secure handling of sensitive data. A secure codebase protects both users and business integrity.
Avoid storing sensitive data in plaintext
Never store passwords, tokens, or other sensitive user data in plaintext. Always hash passwords (e.g., using bcrypt, argon2), encrypt sensitive fields, and avoid logging sensitive values in error traces or logs.
1const hashed = await bcrypt.hash(user.password, 10);
2db.insert({ ...user, password: hashed });
Escape and encode user input
Escape input before outputting it into HTML, attributes, URLs, or query strings to prevent injection attacks. Use well-tested libraries like DOMPurify, xss, or encodeURIComponent as appropriate for the context.
1const cleanComment = xss(req.body.comment);
2res.send(`<p>${cleanComment}</p>`);
Never trust the client
Client-side logic and validation can be bypassed. Always revalidate and sanitize everything on the server, regardless of front-end checks. This is essential to maintain integrity and security.
Set secure HTTP headers
Configure secure HTTP headers to reduce attack surfaces. Use libraries like helmet
(Express) or equivalent frameworks for Next.js, Fastify, etc.
Recommended headers include:
1import helmet from "helmet";
2app.use(helmet());
Implement proper CORS policies
Don't allow unrestricted CORS. Always specify allowed origins and methods. Avoid setting Access-Control-Allow-Origin: *
unless strictly necessary and safe (e.g. for public GET-only APIs).
app.use(cors()); // Default: allows everything
1app.use(cors({
2 origin: "https://myapp.com",
3 methods: ["GET", "POST"]
4}));
Use secure cookie flags
Set the appropriate flags on cookies to prevent session hijacking or cross-site attacks:
HttpOnly
: Prevents access via JSSecure
: Only over HTTPSSameSite
: Controls cross-origin sending
1res.cookie("token", token, {
2 httpOnly: true,
3 secure: true,
4 sameSite: "strict"
5});
Protect against CSRF attacks
Use anti-CSRF tokens for any state-changing requests (POST, PUT, DELETE) — especially when using cookies for authentication. Also consider setting SameSite
cookie flag where possible.
1// Express + csurf
2import csurf from "csurf";
3app.use(csurf({ cookie: true }));
Use rate limiting and throttling
Protect routes from abuse or brute-force attempts with IP- or token-based rate limiting. Especially important for auth, registration, and public APIs.
1import rateLimit from "express-rate-limit";
2const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });
3app.use(limiter);
Avoid information leakage
Don’t expose stack traces, internal errors, environment variables, or implementation details to clients. Log detailed errors on the server, but return only minimal error messages to the client.
res.status(500).json({ error: err }); // Leaks internal structure
1res.status(500).json({ error: "Something went wrong." });
2log.error(err); // Safe internal logging
Keep dependencies up to date
Regularly update third-party packages and monitor them for known vulnerabilities. Use tools like npm audit
, yarn audit
, or GitHub's Dependabot to stay informed. Outdated packages are a major source of exploits.
Enable auto-security updates
Review
changelog
before major bumpsLock down versions for reproducibility
Avoid using 'eval', 'new Function', or unsafe 'Regex'
Avoid using eval
, Function
, or dynamic code execution — they expose your app to injection risks. Similarly, use ReDoS-safe regular expressions and validate untrusted patterns.
eval(userInput); // Vulnerable to injection
const safeOptions = JSON.parse(input); // Safe if schema-validated
Use parameterized queries to prevent injection
Never build SQL or NoSQL queries via string interpolation. Always use parameterized statements or query builders (e.g., Prisma, Knex, Mongoose) to prevent injection attacks.
const result = db.query(`SELECT * FROM users WHERE name = '${name}'`);
const result = db.query("SELECT * FROM users WHERE name = $1", [name]);
Secure secrets and configuration
Never hardcode secrets, tokens, API keys, or credentials in your codebase. Use environment variables or a secure secrets manager (e.g., Vault, AWS Secrets Manager, Doppler). Limit access based on roles or environments.
const jwtSecret = "supersecret123"; // hardcoded
const jwtSecret = process.env.JWT_SECRET;
Validate file uploads and limit size/type
Check file type (MIME & extension), limit upload size, and store files securely (e.g., not publicly accessible without auth). Always assume uploads may be malicious.
1upload.single("avatar", {
2 limits: { fileSize: 5 * 1024 * 1024 },
3 fileFilter: (req, file, cb) => {
4 if (!["image/png", "image/jpeg"].includes(file.mimetype)) {
5 return cb(new Error("Unsupported file type"));
6 }
7 cb(null, true);
8 },
9});
Log security events separately
Track security-related events (e.g., failed logins, permission denials, rate-limit hits) in dedicated logs. These should be tamper-proof and monitored regularly for suspicious patterns.
Note: Avoid logging sensitive payloads (like full auth headers or tokens).
logger.security.warn("Failed login for userId: %s", userId);