Actionable advice and code snippets to harden your web application against common vulnerabilities.
Security headers are HTTP response headers that, when set, can enhance the security of your web application by enabling browser security policies.
Strict-Transport-Security: max-age=31536000; includeSubDomains Content-Security-Policy: default-src 'self' X-Content-Type-Options: nosniff X-Frame-Options: DENY Referrer-Policy: no-referrer-when-downgrade
HTTPS encrypts the data sent between your users and your website, preventing attackers from intercepting sensitive information.
Content Security Policy is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting (XSS) and data injection attacks.
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' https://trusted-cdn.com; img-src 'self' data:;
Forms are a common entry point for attacks. Ensure all forms use HTTPS, implement CSRF protection, and validate input both client-side and server-side.
<form method="POST" action="https://example.com/submit" autocomplete="off">
<!-- Add CSRF token -->
<input type="hidden" name="_csrf" value="{{ csrfToken }}" />
<!-- Other form fields -->
</form>Geo-blocking can help prevent attacks from high-risk regions. Consider blocking traffic from countries commonly associated with cyber threats if they are not part of your target audience.
# Countries commonly associated with cyber threats: - North Korea (KP) - China (CN) - Russia (RU) - Iran (IR) - Syria (SY) - Vietnam (VN) - Nigeria (NG) - Belarus (BY) - Turkey (TR)
There are several ways to implement geo-blocking depending on your infrastructure.
# Cloudflare: Use Firewall Rules # NGINX: Use the geoip module # .htaccess: Manually block IP ranges by country # Security Plugins: Many platforms like WordPress offer geo-blocking plugins
Subresource Integrity (SRI) is a security feature that enables browsers to verify that resources they fetch are delivered without unexpected manipulation.
<script src="https://example.com/example-framework.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>HSTS tells browsers to only use HTTPS, preventing protocol downgrade attacks and cookie hijacking.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Two-factor authentication adds an extra layer of security by requiring users to provide two forms of identification before accessing their accounts.
// Example using speakeasy for TOTP
const speakeasy = require('speakeasy');
// Generate secret
const secret = speakeasy.generateSecret({ length: 20 });
// Verify token
const verified = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: userToken
});Never store passwords in plain text. Use strong hashing algorithms like bcrypt, scrypt, or Argon2 with proper salting.
// Using bcrypt in Node.js
const bcrypt = require('bcrypt');
const saltRounds = 12;
// Hash password
const hash = await bcrypt.hash(password, saltRounds);
// Verify password
const match = await bcrypt.compare(password, hash);Proper session management prevents session hijacking and fixation attacks. Regenerate session IDs after login, set appropriate timeouts, and invalidate sessions on logout.
// Express.js session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000 // 1 hour
}
}));Rate limiting protects your API from abuse, brute force attacks, and denial of service. Implement limits based on IP, user, or API key.
// Express rate limiter
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per window
message: 'Too many requests, please try again later.'
});
app.use('/api/', limiter);JSON Web Tokens (JWT) provide stateless authentication for APIs. Use short expiration times, secure signing algorithms, and never store sensitive data in the payload.
// JWT best practices
const jwt = require('jsonwebtoken');
// Sign token with RS256 (asymmetric)
const token = jwt.sign(
{ userId: user.id, role: user.role },
privateKey,
{ algorithm: 'RS256', expiresIn: '15m' }
);
// Always verify tokens
const decoded = jwt.verify(token, publicKey);Cross-Origin Resource Sharing (CORS) controls which domains can access your API. Never use wildcard (*) in production for sensitive endpoints.
// Express CORS configuration
const cors = require('cors');
const corsOptions = {
origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));Cookies should be configured with security flags to prevent theft and misuse. Always use Secure, HttpOnly, and SameSite attributes.
Set-Cookie: sessionId=abc123; Secure; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600
// In Express.js
res.cookie('sessionId', 'abc123', {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000
});Cookie prefixes (__Secure- and __Host-) provide additional security guarantees enforced by browsers.
// __Secure- prefix: requires Secure flag Set-Cookie: __Secure-sessionId=abc123; Secure; Path=/ // __Host- prefix: requires Secure, no Domain, Path must be / Set-Cookie: __Host-sessionId=abc123; Secure; Path=/
SQL injection is one of the most common and dangerous vulnerabilities. Always use parameterized queries or prepared statements, never concatenate user input into SQL.
// BAD - vulnerable to SQL injection
const query = `SELECT * FROM users WHERE id = ${userId}`;
// GOOD - parameterized query
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);
// GOOD - using ORM (Prisma example)
const user = await prisma.user.findUnique({
where: { id: userId }
});Secure your database connections with SSL/TLS, use least-privilege accounts, and never expose databases directly to the internet.
// PostgreSQL with SSL
const pool = new Pool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
ssl: {
rejectUnauthorized: true,
ca: fs.readFileSync('/path/to/ca-cert.pem')
}
});Always validate and sanitize user input on both client and server side. Use allowlists over denylists when possible.
// Using Zod for validation
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
age: z.number().min(18).max(120),
username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/)
});
// Validate input
const result = userSchema.safeParse(userInput);