Security must be built in from the start, not added later.

Essential Packages

  npm install helmet cors express-rate-limit bcrypt jsonwebtoken
  
  import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';

app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100
});
app.use('/api/', limiter);
  

Password Hashing

Never store plain-text passwords:

  import bcrypt from 'bcrypt';

// Register
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash(password, salt);

// Login
const isValid = await bcrypt.compare(inputPassword, user.password);
  

JWT Authentication

  import jwt from 'jsonwebtoken';

function generateToken(userId) {
    return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '7d' });
}

function verifyToken(req, res, next) {
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) return res.status(401).json({ error: 'No token' });

    try {
        req.user = jwt.verify(token, process.env.JWT_SECRET);
        next();
    } catch {
        res.status(401).json({ error: 'Invalid token' });
    }
}
  

Input Validation

Always validate and sanitize user input:

  import { body, validationResult } from 'express-validator';

app.post('/register',
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }),
    (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
        // Proceed...
    }
);
  

SQL/NoSQL Injection Prevention

Use parameterized queries — ORMs like Prisma and Mongoose handle this:

  // BAD — never do this
db.query(`SELECT * FROM users WHERE email = '${email}'`);

// GOOD — use ORM
await User.findOne({ email });
  

Security Checklist

  • Use HTTPS in production
  • Store secrets in environment variables
  • Hash passwords with bcrypt (cost factor ≥ 12)
  • Validate all input
  • Rate limit auth endpoints
  • Set secure cookie flags (httpOnly, secure, sameSite)
  • Keep dependencies updated (npm audit)
  • Don’t expose stack traces in production
  • Implement proper CORS policy
  • Use helmet for security headers

See also Security in JavaScript for client-side security topics.

Environment Variable Safety

Never hardcode secrets:

  // .env (not committed to git)
JWT_SECRET=your-256-bit-secret
DATABASE_URL=postgres://user:pass@localhost/db

// app.js
import 'dotenv/config';
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET is required');
  

Add .env to .gitignore and provide .env.example with placeholder keys.

Dependency Auditing

  npm audit
npm audit fix
npx snyk test
  

Run audits in CI and block merges on critical vulnerabilities.

Secure HTTP Headers with Helmet

Helmet sets headers that mitigate common attacks:

  • X-Content-Type-Options: nosniff
  • Strict-Transport-Security (HSTS)
  • X-Frame-Options: DENY (clickjacking protection)

Session Security with express-session

  import session from 'express-session';

app.use(session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
        secure: process.env.NODE_ENV === 'production',
        httpOnly: true,
        sameSite: 'strict',
        maxAge: 24 * 60 * 60 * 1000
    }
}));
  

Logging Without Leaking Secrets

  // Bad
console.log('Login attempt', { email, password });

// Good
console.log('Login attempt', { email, success: false });
  

Redact passwords, tokens, and PII from logs.