Security Best Practices for Full-Stack Applications

Security Best Practices for Full-Stack Applications

BySanjay Goraniya
3 min read
Share:

Security Best Practices for Full-Stack Applications

Security isn't optional—it's fundamental. A single vulnerability can compromise your entire application and user data. After dealing with security incidents and implementing security measures in production systems, I've learned what actually matters.

The Security Mindset

Security is not a feature you add—it's a way of thinking. Every decision should consider security implications.

Principles

  1. Defense in depth - Multiple layers of security
  2. Least privilege - Minimum access necessary
  3. Fail securely - Default to secure state
  4. Never trust input - Validate everything
  5. Keep it simple - Complexity breeds vulnerabilities

Authentication and Authorization

Password Security

Code
// Bad: Plain text passwords
const user = {
  email: 'user@example.com',
  password: 'password123' // Never!
};

// Good: Hashed passwords
const bcrypt = require('bcrypt');

async function createUser(email, password) {
  const hashedPassword = await bcrypt.hash(password, 10);
  // Store hashedPassword, never plain text
}

async function verifyPassword(plainPassword, hashedPassword) {
  return await bcrypt.compare(plainPassword, hashedPassword);
}

JWT Best Practices

Code
// Good: Secure JWT implementation
const jwt = require('jsonwebtoken');

// Generate token
function generateToken(user) {
  return jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET,
    {
      expiresIn: '1h',
      issuer: 'your-app',
      audience: 'your-app-users'
    }
  );
}

// Verify token
function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET, {
      issuer: 'your-app',
      audience: 'your-app-users'
    });
  } catch (error) {
    throw new Error('Invalid token');
  }
}

Session Management

Code
// Good: Secure session configuration
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true, // HTTPS only
    httpOnly: true, // Not accessible via JavaScript
    maxAge: 3600000, // 1 hour
    sameSite: 'strict' // CSRF protection
  }
}));

Input Validation

Never Trust User Input

Code
// Bad: No validation
app.post('/api/users', (req, res) => {
  const user = req.body;
  db.query('INSERT INTO users VALUES (?, ?)', [user.email, user.name]);
});

// Good: Validate everything
const { body, validationResult } = require('express-validator');

app.post('/api/users',
  [
    body('email').isEmail().normalizeEmail(),
    body('name').trim().isLength({ min: 1, max: 100 }).escape(),
    body('age').isInt({ min: 0, max: 120 })
  ],
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Process validated input
  }
);

SQL Injection Prevention

Code
// Bad: SQL injection vulnerability
app.get('/api/users', (req, res) => {
  const query = `SELECT * FROM users WHERE id = ${req.query.id}`;
  db.query(query); // Dangerous!
});

// Good: Parameterized queries
app.get('/api/users', (req, res) => {
  db.query('SELECT * FROM users WHERE id = $1', [req.query.id]);
});

XSS Prevention

Code
// Bad: XSS vulnerability
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h1>Results for: ${query}</h1>`); // Dangerous!
});

// Good: Escape output
const escapeHtml = require('escape-html');

app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q);
  res.send(`<h1>Results for: ${query}</h1>`);
});

// Or use a template engine that auto-escapes
res.render('search', { query: req.query.q }); // Auto-escaped

Data Protection

Encryption at Rest

Code
// Encrypt sensitive data
const crypto = require('crypto');

function encrypt(text) {
  const algorithm = 'aes-256-gcm';
  const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
  const iv = crypto.randomBytes(16);
  
  const cipher = crypto.createCipheriv(algorithm, key, iv);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  
  const authTag = cipher.getAuthTag();
  
  return {
    encrypted,
    iv: iv.toString('hex'),
    authTag: authTag.toString('hex')
  };
}

Encryption in Transit

Always use HTTPS:

Code
// Force HTTPS
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    res.redirect(`https://${req.header('host')}${req.url}`);
  } else {
    next();
  }
});

Sensitive Data Handling

Code
// Never log sensitive data
function logUserAction(userId, action) {
  logger.info('User action', {
    userId, // OK
    action, // OK
    // password: user.password, // Never!
    // creditCard: order.cardNumber, // Never!
    // ssn: user.ssn // Never!
  });
}

API Security

Rate Limiting

Code
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP'
});

app.use('/api/', limiter);

CORS Configuration

Code
// Good: Restrictive CORS
const cors = require('cors');

app.use(cors({
  origin: process.env.ALLOWED_ORIGINS.split(','),
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

API Key Security

Code
// Store API keys securely
const apiKeys = new Map();

function validateApiKey(req, res, next) {
  const apiKey = req.header('X-API-Key');
  
  if (!apiKey || !apiKeys.has(apiKey)) {
    return res.status(401).json({ error: 'Invalid API key' });
  }
  
  // Rate limit per API key
  const keyData = apiKeys.get(apiKey);
  if (keyData.requests > keyData.limit) {
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }
  
  keyData.requests++;
  next();
}

Dependency Security

Keep Dependencies Updated

Code
# Check for vulnerabilities
npm audit

# Fix automatically
npm audit fix

# Update dependencies
npm update

Use Lock Files

Code
// package-lock.json should be committed
// Ensures consistent, secure versions

Review Dependencies

  • Check package popularity
  • Review maintenance status
  • Check for known vulnerabilities
  • Prefer well-maintained packages

Error Handling

Don't Leak Information

Code
// Bad: Leaks stack trace
app.use((err, req, res, next) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack // Never in production!
  });
});

// Good: Generic error messages
app.use((err, req, res, next) => {
  logger.error('Error', { error: err, requestId: req.id });
  
  res.status(500).json({
    error: 'An error occurred',
    requestId: req.id // For tracking
  });
});

Security Headers

Code
const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"]
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

Real-World Example

Challenge: E-commerce platform handling payment data, frequent security audits.

Security Measures Implemented:

  1. HTTPS everywhere - All traffic encrypted
  2. Input validation - All user input validated
  3. Parameterized queries - No SQL injection
  4. Rate limiting - Prevent abuse
  5. Secure sessions - HttpOnly, Secure cookies
  6. Dependency updates - Regular security patches
  7. Security headers - Helmet.js configuration
  8. Error handling - No information leakage
  9. Logging - Security events logged
  10. Regular audits - Security testing

Result: Passed security audits, no security incidents.

Security Checklist

  • Passwords hashed (bcrypt, argon2)
  • HTTPS enforced
  • Input validated
  • SQL injection prevented
  • XSS prevented
  • CSRF protection
  • Rate limiting
  • Secure headers
  • Dependencies updated
  • Secrets in environment variables
  • Error messages don't leak info
  • Authentication implemented correctly
  • Authorization checks in place
  • Sensitive data encrypted
  • Security logging enabled

Best Practices Summary

  1. Validate input - Never trust user data
  2. Use HTTPS - Encrypt in transit
  3. Hash passwords - Never store plain text
  4. Keep dependencies updated - Security patches
  5. Limit access - Least privilege principle
  6. Log security events - Monitor for issues
  7. Regular audits - Test your security
  8. Stay informed - Security is evolving

Conclusion

Security is not a one-time task—it's an ongoing process. The threats evolve, and so must your defenses. The key is to:

  • Think security first - Consider security in every decision
  • Follow best practices - Don't reinvent the wheel
  • Stay updated - Security landscape changes
  • Test regularly - Find vulnerabilities before attackers

Remember: Security is everyone's responsibility. Every developer should understand security basics and apply them consistently.

What security challenges have you faced? What practices have been most effective for your applications?

Share:

Related Posts