Web security is critical for protecting applications and users. This guide covers essential security concepts, common vulnerabilities, and best practices.
OWASP Top 10 #
The Open Web Application Security Project lists the most critical security risks.
1. Injection Attacks #
SQL Injection #
Attackers insert malicious SQL code.
// Vulnerable
app.get('/user', (req, res) => {
const query = `SELECT * FROM users WHERE id = ${req.query.id}`;
db.query(query); // Dangerous!
});
// Safe - Use parameterized queries
app.get('/user', (req, res) => {
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [req.query.id]);
});Prevention:
- Use parameterized queries
- Use ORMs with proper escaping
- Validate and sanitize input
- Use least privilege database accounts
NoSQL Injection #
// Vulnerable
db.users.find({ username: req.body.username });
// If input is: {"$ne": null}
// Query becomes: find({ username: {"$ne": null} })
// Safe
const username = String(req.body.username);
db.users.find({ username: username });2. Cross-Site Scripting (XSS) #
Inject malicious scripts into web pages.
Reflected XSS #
// Vulnerable
app.get('/search', (req, res) => {
res.send(`Results for: ${req.query.q}`);
});
// Attack: /search?q=<script>alert('XSS')</script>
// Safe - Escape output
app.get('/search', (req, res) => {
const escaped = escapeHtml(req.query.q);
res.send(`Results for: ${escaped}`);
});Stored XSS #
// Vulnerable
app.post('/comment', (req, res) => {
const comment = req.body.text;
db.save({ text: comment }); // Stores malicious script
});
// Safe - Sanitize before storing
const DOMPurify = require('isomorphic-dompurify');
app.post('/comment', (req, res) => {
const clean = DOMPurify.sanitize(req.body.text);
db.save({ text: clean });
});DOM XSS #
// Vulnerable
const search = window.location.search;
document.getElementById('result').innerHTML = search;
// Safe
document.getElementById('result').textContent = search;Prevention:
- Escape output based on context (HTML, JavaScript, CSS, URL)
- Use Content Security Policy (CSP)
- Sanitize user input
- Use
textContentinstead ofinnerHTML - Use frameworks that auto-escape (React, Vue)
3. Cross-Site Request Forgery (CSRF) #
Force users to execute unwanted actions.
// Vulnerable
app.post('/transfer', (req, res) => {
const { to, amount } = req.body;
transfer(req.user.id, to, amount);
});
// Attack via malicious site:
// <form action="https://bank.com/transfer" method="POST">
// <input name="to" value="attacker">
// <input name="amount" value="1000">
// </form>
// Safe - Use CSRF tokens
const csrf = require('csurf');
app.use(csrf());
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer', (req, res) => {
// Token automatically validated
transfer(req.user.id, req.body.to, req.body.amount);
});Prevention:
- Use CSRF tokens
- Check
Refererheader - Use
SameSitecookie attribute - Require re-authentication for sensitive actions
4. Authentication Issues #
Weak Passwords #
const bcrypt = require('bcrypt');
// Hash passwords
async function createUser(username, password) {
const hash = await bcrypt.hash(password, 10);
await db.users.insert({ username, password: hash });
}
// Verify passwords
async function login(username, password) {
const user = await db.users.findOne({ username });
const valid = await bcrypt.compare(password, user.password);
return valid;
}Session Management #
const session = require('express-session');
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'
}
}));Prevention:
- Hash passwords with bcrypt, argon2, or scrypt
- Enforce strong password policies
- Implement rate limiting
- Use MFA (Multi-Factor Authentication)
- Secure session management
- Implement account lockout
5. Sensitive Data Exposure #
// Vulnerable
app.get('/api/user', (req, res) => {
const user = await db.users.findById(req.userId);
res.json(user); // Sends password hash, etc.
});
// Safe - Filter sensitive data
app.get('/api/user', (req, res) => {
const user = await db.users.findById(req.userId);
const { password, ssn, ...safe } = user;
res.json(safe);
});Prevention:
- Use HTTPS everywhere
- Encrypt sensitive data at rest
- Don’t store unnecessary sensitive data
- Use secure key management
- Implement proper access controls
6. XML External Entities (XXE) #
const libxmljs = require('libxmljs');
// Vulnerable
const xml = libxmljs.parseXml(userInput);
// Safe - Disable external entities
const xml = libxmljs.parseXml(userInput, {
noent: false,
dtdload: false,
dtdvalid: false
});7. Broken Access Control #
// Vulnerable
app.get('/api/user/:id', (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user); // Any user can access any profile
});
// Safe - Check authorization
app.get('/api/user/:id', (req, res) => {
if (req.params.id !== req.session.userId && !req.session.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = await db.users.findById(req.params.id);
res.json(user);
});Prevention:
- Deny by default
- Implement proper authorization checks
- Don’t rely on client-side access control
- Use object-level permissions
- Log access control failures
8. Security Misconfiguration #
// Vulnerable
app.use((err, req, res, next) => {
res.status(500).json({ error: err.stack });
});
// Safe
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal server error' });
});Prevention:
- Remove default credentials
- Disable directory listing
- Keep software updated
- Configure security headers
- Remove unnecessary features
- Don’t expose stack traces in production
9. Using Components with Known Vulnerabilities #
# Check for vulnerabilities
npm audit
# Fix vulnerabilities
npm audit fix
# Monitor dependencies
npm install -g snyk
snyk test10. Insufficient Logging & Monitoring #
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Log security events
app.post('/login', async (req, res) => {
const success = await attemptLogin(req.body);
if (!success) {
logger.warn('Failed login attempt', {
username: req.body.username,
ip: req.ip,
timestamp: new Date()
});
}
});Security Headers #
const helmet = require('helmet');
app.use(helmet());
// Or configure individually
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:']
}
}));
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
app.use(helmet.noSniff());
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.xssFilter());Important Headers #
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: no-referrer
Permissions-Policy: geolocation=(), microphone=()Input Validation #
const validator = require('validator');
// Email validation
function validateEmail(email) {
return validator.isEmail(email);
}
// URL validation
function validateURL(url) {
return validator.isURL(url, {
protocols: ['http', 'https'],
require_protocol: true
});
}
// Sanitize input
function sanitizeInput(input) {
return validator.escape(validator.trim(input));
}
// Complex validation
const { body, validationResult } = require('express-validator');
app.post('/register',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('username').isAlphanumeric().trim(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Proceed with registration
}
);Rate Limiting #
const rateLimit = require('express-rate-limit');
// General rate limit
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // Limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Strict limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts'
});
app.post('/login', authLimiter, (req, res) => {
// Login logic
});API Security #
JWT Best Practices #
const jwt = require('jsonwebtoken');
// Create token
function createToken(userId) {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{
expiresIn: '1h',
algorithm: 'HS256'
}
);
}
// Verify token
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (err) {
return null;
}
}
// Middleware
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = verifyToken(token);
if (!decoded) {
return res.status(401).json({ error: 'Invalid token' });
}
req.userId = decoded.userId;
next();
}API Keys #
const crypto = require('crypto');
// Generate API key
function generateApiKey() {
return crypto.randomBytes(32).toString('hex');
}
// Store hashed
const hash = crypto
.createHash('sha256')
.update(apiKey)
.digest('hex');
// Verify API key middleware
async function verifyApiKey(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'No API key' });
}
const hash = crypto
.createHash('sha256')
.update(apiKey)
.digest('hex');
const valid = await db.apiKeys.findOne({ hash });
if (!valid) {
return res.status(401).json({ error: 'Invalid API key' });
}
next();
}File Upload Security #
const multer = require('multer');
const path = require('path');
const upload = multer({
storage: multer.diskStorage({
destination: 'uploads/',
filename: (req, file, cb) => {
// Generate random filename
const randomName = crypto.randomBytes(16).toString('hex');
cb(null, randomName + path.extname(file.originalname));
}
}),
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
},
fileFilter: (req, file, cb) => {
// Allow only specific types
const allowed = ['.jpg', '.jpeg', '.png', '.pdf'];
const ext = path.extname(file.originalname).toLowerCase();
if (allowed.includes(ext)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ filename: req.file.filename });
});Database Security #
// Use parameterized queries
const userId = req.params.id;
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);
// MongoDB - Avoid $where
// Bad
db.users.find({ $where: `this.username == '${username}'` });
// Good
db.users.find({ username: username });
// Principle of least privilege
// Create read-only user for queries
CREATE USER 'readonly'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON database.* TO 'readonly'@'localhost';Encryption #
const crypto = require('crypto');
// Encrypt data
function encrypt(text, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
// Decrypt data
function decrypt(encrypted, key) {
const parts = encrypted.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encryptedText = parts[1];
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}CORS Security #
const cors = require('cors');
// Restrictive CORS
app.use(cors({
origin: ['https://trusted-domain.com'],
credentials: true,
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Dynamic origin validation
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));Environment Variables #
// .env file
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key-here
API_KEY=your-api-key
// Load securely
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;
// Never commit .env to git
// Add to .gitignore:
# .env
# .env.localSecurity Checklist #
Authentication #
- Hash passwords with bcrypt/argon2
- Implement rate limiting on login
- Use secure session management
- Implement MFA
- Enforce strong password policy
Authorization #
- Check permissions on every request
- Use role-based access control
- Implement object-level permissions
- Default deny access
Data Protection #
- Use HTTPS everywhere
- Encrypt sensitive data at rest
- Use secure headers (Helmet)
- Implement CSP
- Sanitize user input
API Security #
- Validate all inputs
- Use rate limiting
- Implement CORS properly
- Use authentication (JWT/OAuth)
- Version your API
Infrastructure #
- Keep dependencies updated
- Run security audits (npm audit)
- Use environment variables
- Implement logging and monitoring
- Regular security testing
Common Vulnerabilities #
Path Traversal #
// Vulnerable
app.get('/file', (req, res) => {
const file = req.query.name;
res.sendFile(`/uploads/${file}`);
});
// Attack: /file?name=../../../etc/passwd
// Safe
const path = require('path');
app.get('/file', (req, res) => {
const safePath = path.normalize(req.query.name).replace(/^(\.\.(\/|\\|$))+/, '');
const fullPath = path.join('/uploads', safePath);
if (!fullPath.startsWith('/uploads')) {
return res.status(400).send('Invalid path');
}
res.sendFile(fullPath);
});Command Injection #
// Vulnerable
const { exec } = require('child_process');
app.get('/ping', (req, res) => {
exec(`ping -c 4 ${req.query.host}`, (error, stdout) => {
res.send(stdout);
});
});
// Attack: /ping?host=google.com; rm -rf /
// Safe - Don't use exec with user input
// Use libraries or validate heavily
const { spawn } = require('child_process');
app.get('/ping', (req, res) => {
const host = req.query.host;
// Validate input
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
return res.status(400).send('Invalid host');
}
const ping = spawn('ping', ['-c', '4', host]);
let output = '';
ping.stdout.on('data', data => output += data);
ping.on('close', () => res.send(output));
});Open Redirect #
// Vulnerable
app.get('/redirect', (req, res) => {
res.redirect(req.query.url);
});
// Attack: /redirect?url=https://malicious-site.com
// Safe - Whitelist domains
app.get('/redirect', (req, res) => {
const allowed = [
'https://mysite.com',
'https://trusted.com'
];
const url = req.query.url;
if (allowed.some(domain => url.startsWith(domain))) {
res.redirect(url);
} else {
res.status(400).send('Invalid redirect URL');
}
});Best Practices #
- Defense in depth - Multiple security layers
- Principle of least privilege - Minimal necessary access
- Fail securely - Default to secure state
- Keep it simple - Complex = more vulnerabilities
- Don’t trust user input - Validate everything
- Security by design - Not an afterthought
- Stay updated - Monitor security advisories
- Regular audits - Test your security
- Educate team - Security is everyone’s responsibility
- Plan for breaches - Incident response plan
Web security requires constant vigilance. Stay informed about new vulnerabilities and always follow security best practices.