Skip to content
All essays
WebMarch 25, 202417 min

Web Security Best Practices

Essential security practices for frontend applications: XSS, CSRF, CSP, authentication, and secure coding

Ü
Ümit Uz
Mobile & Full Stack Developer

Security is critical for web applications. Let's master essential practices to protect your frontend applications from common vulnerabilities.

Common Vulnerabilities

XSS (Cross-Site Scripting)

What: Attackers inject malicious scripts into web pages viewed by other users.

Prevention:

javascript
// ❌ BAD: Direct HTML injection
div.innerHTML = userInput;

// ✅ GOOD: Use textContent
div.textContent = userInput;

// ✅ GOOD: Sanitize HTML
import DOMPurify from 'dompurify';
div.innerHTML = DOMPurify.sanitize(userInput);

// ✅ GOOD: Use template literals safely
const element = `<div>${escapeHtml(userInput)}</div>>;

function escapeHtml(unsafe) {
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

CSRF (Cross-Site Request Forgery)

What: Attackers trick users into performing actions they didn't intend.

Prevention:

javascript
// Server-side: Generate CSRF token
app.use(session({ secret: 'secret' }));
app.use(csrf());

// Send token to client
app.get('/form', (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// Client-side: Include token in requests
fetch('/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  credentials: 'same-origin'
});

SQL Injection

What: Attackers manipulate database queries through user input.

Prevention:

javascript
// ❌ BAD: String concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;

// ✅ GOOD: Parameterized queries
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);

// ✅ GOOD: ORM
const user = await User.findByPk(userId);

Content Security Policy (CSP)

CSP restricts what resources can be loaded on your page.

html
<!-- HTTP Header -->
meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
>

<!-- Or via server header -->
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com

CSP Best Practices

javascript
// Express.js middleware
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; " +
    "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com; " +
    "style-src 'self' 'unsafe-inline'; " +
    "img-src 'self' data: https:; " +
    "font-src 'self' data:; " +
    "connect-src 'self' https://api.example.com; " +
    "frame-ancestors 'none';"
  );
  next();
});

Authentication & Authorization

JWT (JSON Web Tokens)

javascript
// Generate token
const jwt = require('jsonwebtoken');

const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '1h' }
);

// Verify token
try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  console.log(decoded.userId); // Access user data
} catch (err) {
  // Invalid token
}

Secure Password Storage

javascript
const bcrypt = require('bcrypt');
const saltRounds = 10;

// Hash password
const hash = await bcrypt.hash(plainPassword, saltRounds);

// Compare password
const match = await bcrypt.compare(plainPassword, hash);

Session Management

javascript
// Express session
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true, // HTTPS only
    httpOnly: true, // No JavaScript access
    sameSite: 'strict', // CSRF protection
    maxAge: 3600000 // 1 hour
  }
}));

Secure Headers

javascript
// Helmet.js middleware
const helmet = require('helmet');
app.use(helmet());

// Or manually
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  next();
});

Data Protection

Sensitive Data in URLs

javascript
// ❌ BAD: Sensitive data in URL
fetch(`/api/user?token=${token}`);

// ✅ GOOD: Use POST with body
fetch('/api/user', {
  method: 'POST',
  body: JSON.stringify({ token }),
  headers: { 'Content-Type': 'application/json' }
});

Local Storage Security

javascript
// ⚠️ WARNING: localStorage is accessible by JavaScript
// Don't store sensitive data

// ✅ GOOD: Use httpOnly cookies
res.cookie('token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
});

Environment Variables

javascript
// ✅ GOOD: Use environment variables
const dbPassword = process.env.DB_PASSWORD;

// Never commit .env files
// Add to .gitignore
.env
.env.local
.env.production

HTTPS and SSL

Enforce HTTPS

javascript
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
  if (!req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }
  next();
});

// HSTS header
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

Input Validation

Client-side Validation

javascript
// Validate email
function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// Sanitize input
function sanitizeInput(input) {
  return input.trim().replace(/[<>]/g, '');
}

// Use validation libraries
import Joi from 'joi';

const schema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required()
});

const { error } = schema.validate({ email, password });

Server-side Validation

javascript
// Always validate on server
app.post('/api/users', (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ error: 'Missing fields' });
  }

  if (!isValidEmail(email)) {
    return res.status(400).json({ error: 'Invalid email' });
  }

  if (password.length < 8) {
    return res.status(400).json({ error: 'Password too short' });
  }

  // Create user...
});

Error Handling

Safe Error Messages

javascript
// ❌ BAD: Expose internal details
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

// ✅ GOOD: Generic error message
app.use((err, req, res, next) => {
  console.error(err); // Log details server-side
  res.status(500).json({ error: 'Internal server error' });
});

// ✅ GOOD: Different messages for different environments
const isDev = process.env.NODE_ENV === 'development';

app.use((err, req, res, next) => {
  res.status(err.status || 500).json({
    error: isDev ? err.message : 'Something went wrong',
    ...(isDev && { stack: err.stack })
  });
});

API Security

Rate Limiting

javascript
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
});

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

API Key Authentication

javascript
// Verify API key
app.use('/api', (req, res, next) => {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey || apiKey !== process.env.API_KEY) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  next();
});

Third-party Dependencies

bash
# Check for vulnerabilities
npm audit

# Fix vulnerabilities
npm audit fix

# Review dependencies
npm list

# Update packages
npm update

Security Checklist

  • [ ] HTTPS enabled
  • [ ] Input validation on client and server
  • [ ] Output encoding (prevent XSS)
  • [ ] CSRF protection
  • [ ] Secure password hashing
  • [ ] JWT/token security
  • [ ] Secure headers configured
  • [ ] CSP implemented
  • [ ] Dependencies up to date
  • [ ] Error handling doesn't expose sensitive info
  • [ ] Rate limiting implemented
  • [ ] Authentication properly implemented
  • [ ] Sensitive data not in localStorage
  • [ ] Environment variables used for secrets

Conclusion

Web security is multi-layered. Implement these practices from the start, regularly audit your code, and stay updated on new vulnerabilities. Remember: client-side validation is for UX, server-side validation is for security.

Related essays

Next essay
CSS Animations Complete Guide