Most security vulnerabilities are not the result of sophisticated attacks. They come from ordinary coding mistakes: unsanitized inputs, hardcoded secrets, outdated dependencies, and misconfigured defaults. The good news is that preventing these does not require a security PhD. It requires building a few habits into your daily workflow.
This guide covers the secure coding practices that have the highest impact per effort invested. These are the things that, in my experience reviewing hundreds of codebases, would have prevented the majority of real-world vulnerabilities.
Input Validation and Sanitization
This is number one for a reason. Injection attacks (SQL, XSS, command injection) remain in the OWASP Top 10 year after year because developers keep trusting user input.
The rule is simple: never trust input from any external source. Not from users, not from APIs, not from databases, not from your own frontend.
Validate on the server side, always. Client-side validation is for user experience. Server-side validation is for security. An attacker bypasses your frontend in seconds.
// Bad: trusting input directly
app.get('/user/:id', (req, res) => {
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
});
// Good: parameterized queries
app.get('/user/:id', (req, res) => {
db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
});
Use allowlists over denylists. Instead of trying to block malicious patterns (which is an endless game), define what valid input looks like and reject everything else.
const VALID_SORT_FIELDS = ['name', 'date', 'status'];
function validateSortField(field) {
if (!VALID_SORT_FIELDS.includes(field)) {
throw new ValidationError('Invalid sort field');
}
return field;
}
Validate types, ranges, and formats. Use schema validation libraries like Zod, Joi, or Pydantic to define expected data shapes:
import { z } from 'zod';
const UserInput = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150),
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s'-]+$/),
});
Authentication and Session Management
Never roll your own authentication. Use established libraries and services. The number of ways to get auth wrong is staggering: timing attacks, insecure token generation, improper session invalidation, weak password hashing.
Hash passwords with bcrypt, scrypt, or Argon2. Not MD5. Not SHA-256 without salt. Not even SHA-256 with salt. Use purpose-built password hashing functions with adaptive work factors.
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
Implement proper session management:
- Generate session tokens with cryptographically secure random number generators.
- Set appropriate expiration times.
- Invalidate sessions on logout and password change.
- Use secure, httpOnly, sameSite cookie attributes.
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000, // 1 hour
},
resave: false,
saveUninitialized: false,
}));
Secrets Management
Hardcoded secrets are the most common critical finding in code reviews. I have found AWS keys, database passwords, API tokens, and private keys committed to version control more times than I can count.
Use environment variables or a secrets manager. Never commit secrets to your repository, not even in "private" repositories.
// Bad
const API_KEY = 'sk-live-abc123def456';
// Good
const API_KEY = process.env.API_KEY;
if (!API_KEY) throw new Error('API_KEY is required');
Use a .gitignore and pre-commit hooks to prevent accidental commits:
# Install detect-secrets
pip install detect-secrets
detect-secrets scan > .secrets.baseline
Rotate secrets regularly and ensure your code handles rotation gracefully. If rotating a database password requires a deployment, your architecture needs work.
Dependency Management
Your code is only as secure as your dependencies, and your dependencies are most of your code. A typical Node.js application has hundreds of transitive dependencies.
Pin dependency versions in lock files. Always commit your lock file (package-lock.json, poetry.lock, go.sum). This ensures reproducible builds and prevents supply chain attacks through version range manipulation.
Audit dependencies regularly:
npm audit
pip-audit
cargo audit
Evaluate new dependencies before adopting them. Check:
- Maintenance activity (last commit, open issues, release frequency).
- Download counts and community size.
- Known vulnerabilities.
- License compatibility.
- Number of transitive dependencies it brings in.
Remove unused dependencies. Every dependency is attack surface. Run depcheck for Node.js or equivalent tools for your ecosystem to identify packages you no longer use.
Error Handling and Logging
Never expose stack traces or internal details to users. Generic error messages for users, detailed logs for operators.
app.use((err, req, res, next) => {
// Log the full error internally
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
});
// Return generic message to user
res.status(500).json({
error: 'An internal error occurred',
requestId: req.id,
});
});
Log security-relevant events: failed login attempts, authorization failures, input validation rejections, and configuration changes. These logs are your forensic trail during incident response.
Never log sensitive data. Sanitize logs to remove passwords, tokens, credit card numbers, and personal data.
Cryptography
Use established libraries. Do not implement your own crypto. Do not even implement your own wrapper around crypto primitives unless you deeply understand the pitfalls.
Use current algorithms:
- Symmetric encryption: AES-256-GCM (not ECB mode, ever)
- Hashing: SHA-256 or SHA-3 (not MD5, not SHA-1)
- Password hashing: Argon2id, bcrypt, or scrypt
- Key exchange: X25519 or ECDH with P-256
Generate random values correctly:
// Bad: predictable
const token = Math.random().toString(36);
// Good: cryptographically secure
const crypto = require('crypto');
const token = crypto.randomBytes(32).toString('hex');
Least Privilege
Request minimum permissions. Your database user should not be root. Your service account should not have admin access. Your API token should have the narrowest scope possible.
Separate read and write database users where feasible. A reporting service needs read-only access.
Use role-based access control in your application. Define roles with specific permissions and assign users to roles, rather than granting permissions directly.
Code Review for Security
Add security to your review checklist. When reviewing PRs, look for:
- Unsanitized input flowing to database queries, commands, or HTML output.
- New dependencies (check them for known issues).
- Hardcoded secrets or credentials.
- Proper error handling (no leaked internals).
- Authorization checks on every endpoint.
- Sensitive data handling (encryption at rest, in transit).
How Safeguard.sh Helps
Safeguard.sh continuously monitors your codebase dependencies for known vulnerabilities, giving developers real-time visibility into the security posture of their projects. It integrates into your existing development workflow, flagging vulnerable dependencies during code review and enforcing security policies before code reaches production. By automating the dependency management side of secure coding, Safeguard.sh lets developers focus on writing secure application logic while the platform handles the supply chain risk.