Secure Coding Practices for Web Developers

Written on January 5, 2026 by Qori Akbar Rahmatullah

6 min read

--- views


Introduction

As web developers, we often focus on functionality, performance, and user experience. However, security should be a fundamental consideration from the start of any project. A single security vulnerability can lead to data breaches, financial losses, and damaged reputation.

This guide covers essential secure coding practices that every web developer should implement to build robust and secure applications.

Authentication & Authorization

Secure Password Storage

Never store passwords in plain text. Always use strong hashing algorithms with salt.

// ❌ BAD: Plain text storage
const password = 'user123';
await db.users.create({ password: password });
 
// ✅ GOOD: Using bcrypt with salt
import bcrypt from 'bcrypt';
 
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
await db.users.create({ password: hashedPassword });
 
// Verifying password
const isValid = await bcrypt.compare(inputPassword, hashedPassword);

JWT Best Practices

When using JSON Web Tokens for authentication:

// ✅ GOOD: Secure JWT implementation
import jwt from 'jsonwebtoken';
 
// Use strong secret (store in environment variables)
const secret = process.env.JWT_SECRET; // At least 256 bits
 
// Set appropriate expiration
const token = jwt.sign(
  { userId: user.id },
  secret,
  { expiresIn: '1h' } // Short expiration time
);
 
// Store refresh tokens separately
const refreshToken = jwt.sign(
  { userId: user.id },
  process.env.REFRESH_TOKEN_SECRET,
  { expiresIn: '7d' }
);

Important JWT security tips:

  • Never store sensitive data in JWT payload
  • Use HTTPS only
  • Implement token rotation
  • Store tokens in httpOnly cookies, not localStorage

Input Validation & Sanitization

Preventing XSS (Cross-Site Scripting)

Always sanitize user input before rendering:

// ❌ BAD: Directly rendering user input
function UserProfile({ bio }) {
  return <div dangerouslySetInnerHTML={{ __html: bio }} />;
}
 
// ✅ GOOD: Sanitize before rendering
import DOMPurify from 'dompurify';
 
function UserProfile({ bio }) {
  const sanitizedBio = DOMPurify.sanitize(bio);
  return <div dangerouslySetInnerHTML={{ __html: sanitizedBio }} />;
}
 
// ✅ BETTER: Use React's built-in escaping
function UserProfile({ bio }) {
  return <div>{bio}</div>; // React automatically escapes
}

SQL Injection Prevention

Use parameterized queries or ORMs:

// ❌ BAD: String concatenation
const query = `SELECT * FROM users WHERE email = '${userEmail}'`;
db.execute(query);
 
// ✅ GOOD: Parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.execute(query, [userEmail]);
 
// ✅ BETTER: Using ORM (Prisma example)
const user = await prisma.user.findUnique({
  where: { email: userEmail }
});

API Security

Rate Limiting

Protect your APIs from abuse:

// Using express-rate-limit
import rateLimit from '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, please try again later.',
  standardHeaders: true,
  legacyHeaders: false
});
 
app.use('/api/', limiter);

CORS Configuration

Configure CORS properly:

// ❌ BAD: Allowing all origins
app.use(cors({ origin: '*' }));
 
// ✅ GOOD: Whitelist specific origins
const allowedOrigins = ['https://yourdomain.com', 'https://app.yourdomain.com'];
 
app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error('Not allowed by CORS'));
      }
    },
    credentials: true
  })
);

API Key Management

// ✅ GOOD: Secure API key handling
// Never expose API keys in client-side code
// Use environment variables
const apiKey = process.env.API_KEY;
 
// For server-to-server communication only
const response = await fetch('https://api.service.com/data', {
  headers: {
    Authorization: `Bearer ${apiKey}`
  }
});

Data Protection

Encryption

// ✅ GOOD: Encrypting sensitive data
import crypto from 'crypto';
 
const algorithm = 'aes-256-gcm';
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
 
function encrypt(text) {
  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 {
    iv: iv.toString('hex'),
    encryptedData: encrypted,
    authTag: authTag.toString('hex')
  };
}
 
function decrypt(encrypted) {
  const decipher = crypto.createDecipheriv(
    algorithm,
    key,
    Buffer.from(encrypted.iv, 'hex')
  );
 
  decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
 
  let decrypted = decipher.update(encrypted.encryptedData, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
 
  return decrypted;
}

Environment Variables

// ❌ BAD: Hardcoded secrets
const dbPassword = "mySecretPassword123";
const apiKey = "sk-1234567890abcdef";
 
// ✅ GOOD: Using environment variables
// .env file (add to .gitignore!)
DB_PASSWORD=mySecretPassword123
API_KEY=sk-1234567890abcdef
JWT_SECRET=your-super-secret-key
 
// In your code
const dbPassword = process.env.DB_PASSWORD;
const apiKey = process.env.API_KEY;

Security Headers

Implement essential security headers:

// Using helmet.js in Express
import helmet from 'helmet';
 
app.use(helmet());
 
// Or manually configure headers
app.use((req, res, next) => {
  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');
 
  // Prevent MIME type sniffing
  res.setHeader('X-Content-Type-Options', 'nosniff');
 
  // Enable XSS protection
  res.setHeader('X-XSS-Protection', '1; mode=block');
 
  // HTTPS only
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains'
  );
 
  // Content Security Policy
  res.setHeader('Content-Security-Policy', "default-src 'self'");
 
  next();
});

Common OWASP Vulnerabilities

1. Broken Access Control

// ❌ BAD: No authorization check
app.delete('/api/users/:id', async (req, res) => {
  await db.users.delete(req.params.id);
  res.json({ success: true });
});
 
// ✅ GOOD: Verify user permissions
app.delete('/api/users/:id', authenticateUser, async (req, res) => {
  // Check if user is deleting their own account or is admin
  if (req.user.id !== req.params.id && !req.user.isAdmin) {
    return res.status(403).json({ error: 'Forbidden' });
  }
 
  await db.users.delete(req.params.id);
  res.json({ success: true });
});

2. Cryptographic Failures

// ❌ BAD: Using weak encryption
const hash = crypto.createHash('md5').update(password).digest('hex');
 
// ✅ GOOD: Using strong algorithms
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 10);

3. Injection Attacks

Already covered in SQL Injection section, but also applies to:

  • Command Injection
  • LDAP Injection
  • XML Injection

General rule: Never trust user input. Always validate and sanitize.

Security Checklist

Before deploying your application, ensure:

  • [ ] All passwords are hashed with bcrypt or argon2
  • [ ] Input validation on both client and server
  • [ ] Parameterized queries for database operations
  • [ ] HTTPS enabled (redirect HTTP to HTTPS)
  • [ ] Security headers configured
  • [ ] CORS properly configured
  • [ ] Rate limiting implemented
  • [ ] Secrets stored in environment variables
  • [ ] Dependencies regularly updated
  • [ ] Error messages don't expose sensitive info
  • [ ] Authentication & authorization properly implemented
  • [ ] File uploads validated and sanitized
  • [ ] Logging and monitoring in place

Tools & Resources

Security Testing Tools:

  • OWASP ZAP - Web application security scanner
  • Burp Suite - Security testing platform
  • npm audit - Check for vulnerable dependencies
  • Snyk - Continuous security monitoring

Learning Resources:

Conclusion

Security is not a one-time task but an ongoing process. By implementing these secure coding practices from the start, you significantly reduce the risk of vulnerabilities in your applications.

Remember:

  • Defense in depth - Multiple layers of security
  • Principle of least privilege - Grant minimum necessary permissions
  • Never trust user input - Always validate and sanitize
  • Keep dependencies updated - Regularly patch vulnerabilities
  • Security is everyone's responsibility - Not just the security team

Stay secure, and happy coding! 🔒

Other posts you might like

← Back to blogEdit this on GitHub