Meta Description: Secure your REST API with this comprehensive checklist covering authentication, authorization, encryption, rate limiting, input validation, and more.
Keywords: api security, security best practices, api security checklist, secure api design, api hardening
Word Count: ~2,300 words
Your API is live. But is it secure?
Security isn't one feature—it's a collection of practices. Miss one, and attackers find a way in.
Here's a complete security checklist for REST APIs.
Authentication & Authorization
✓ Use HTTPS Everywhere
Never use HTTP for APIs. Always use HTTPS (TLS 1.2 or higher).
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.protocol !== 'https' && process.env.NODE_ENV === 'production') {
return res.redirect(301, `https://${req.hostname}${req.url}`);
}
next();
});
✓ Implement Proper Authentication
Choose the right method: - API Keys: Server-to-server - OAuth 2.0: User authorization - JWT: Stateless authentication
Never accept credentials in URLs:
❌ GET /api/pets?api_key=abc123
✓ GET /api/pets
Authorization: Bearer abc123
✓ Verify Object-Level Authorization
Always check if the user owns or has permission to access the resource:
app.get('/v1/pets/:id', async (req, res) => {
const pet = await db.pets.findById(req.params.id);
if (!pet) {
return res.status(404).json({ error: 'Not found' });
}
// Check ownership
if (pet.ownerId !== req.auth.userId && !req.auth.isAdmin) {
return res.status(403).json({ error: 'Access denied' });
}
res.json(pet);
});
✓ Implement Function-Level Authorization
Protect admin and privileged endpoints:
function requireAdmin(req, res, next) {
if (!req.auth.scopes.includes('admin:all')) {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
app.delete('/v1/admin/users/:id', requireAdmin, async (req, res) => {
// Only admins can delete users
});
✓ Use Short-Lived Tokens
Tokens should expire:
const token = jwt.sign(
{ userId: user.id },
JWT_SECRET,
{ expiresIn: '1h' } // Expire after 1 hour
);
Provide refresh tokens for long-lived sessions:
const accessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: '7d' });
Input Validation
✓ Validate All Input
Never trust client input. Validate everything:
const { body, validationResult } = require('express-validator');
app.post('/v1/pets',
body('name').isString().trim().isLength({ min: 1, max: 100 }),
body('species').isIn(['DOG', 'CAT', 'BIRD', 'RABBIT']),
body('age').optional().isInt({ min: 0, max: 30 }),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
// Process valid input
}
);
✓ Sanitize Input
Remove dangerous characters:
const sanitizeHtml = require('sanitize-html');
function sanitizeInput(input) {
if (typeof input === 'string') {
return sanitizeHtml(input, {
allowedTags: [], // Strip all HTML
allowedAttributes: {}
});
}
return input;
}
✓ Prevent SQL Injection
Use parameterized queries:
// ❌ Vulnerable to SQL injection
const pets = await db.query(`SELECT * FROM pets WHERE name = '${req.query.name}'`);
// ✓ Safe with parameterized query
const pets = await db.query('SELECT * FROM pets WHERE name = ?', [req.query.name]);
✓ Prevent NoSQL Injection
Validate types in NoSQL queries:
// ❌ Vulnerable
const user = await db.users.findOne({ email: req.body.email });
// ✓ Safe
const user = await db.users.findOne({
email: String(req.body.email) // Ensure it's a string
});
✓ Limit Request Size
Prevent large payload attacks:
app.use(express.json({ limit: '1mb' })); // Max 1MB request body
app.use(express.urlencoded({ limit: '1mb', extended: true }));
Rate Limiting
✓ Implement Rate Limiting
Protect against brute force and DoS:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests' }
});
app.use('/v1/', limiter);
✓ Stricter Limits for Sensitive Endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Only 5 login attempts per 15 minutes
skipSuccessfulRequests: true
});
app.post('/v1/auth/login', authLimiter, async (req, res) => {
// Handle login
});
✓ Return Rate Limit Headers
app.use((req, res, next) => {
res.setHeader('X-RateLimit-Limit', '100');
res.setHeader('X-RateLimit-Remaining', '95');
res.setHeader('X-RateLimit-Reset', '1678886400');
next();
});
Data Protection
✓ Never Log Sensitive Data
// ❌ Don't log passwords, tokens, or PII
console.log('Login attempt:', req.body); // Contains password!
// ✓ Log safely
console.log('Login attempt:', { email: req.body.email });
✓ Hash Passwords
Never store plain text passwords:
const bcrypt = require('bcrypt');
async function hashPassword(password) {
return await bcrypt.hash(password, 12); // 12 rounds
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
✓ Encrypt Sensitive Data at Rest
const crypto = require('crypto');
function encrypt(text, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return { encrypted, iv, tag };
}
function decrypt(encrypted, iv, tag, key) {
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
return decipher.update(encrypted) + decipher.final('utf8');
}
✓ Filter Sensitive Fields from Responses
function sanitizeUser(user) {
const { passwordHash, ssn, creditCard, ...safeUser } = user;
return safeUser;
}
app.get('/v1/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(sanitizeUser(user));
});
Error Handling
✓ Don't Expose Stack Traces
// ❌ Exposes internal details
app.use((err, req, res, next) => {
res.status(500).json({ error: err.stack });
});
// ✓ Generic error message
app.use((err, req, res, next) => {
console.error(err); // Log internally
res.status(500).json({
type: 'https://api.petstoreapi.com/errors/internal-error',
title: 'Internal Server Error',
status: 500,
detail: 'An unexpected error occurred'
});
});
✓ Use Standard Error Format
Follow RFC 9457:
function errorResponse(type, title, status, detail) {
return {
type: `https://api.petstoreapi.com/errors/${type}`,
title,
status,
detail
};
}
Headers & CORS
✓ Set Security Headers
const helmet = require('helmet');
app.use(helmet()); // Sets multiple security headers
// Or manually:
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
✓ Configure CORS Properly
const cors = require('cors');
app.use(cors({
origin: ['https://myapp.com'], // Specific origins, not '*'
credentials: true,
maxAge: 86400
}));
Monitoring & Logging
✓ Log Security Events
function logSecurityEvent(event, details) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
event,
...details
}));
}
// Log failed auth attempts
app.post('/v1/auth/login', async (req, res) => {
const user = await authenticateUser(req.body);
if (!user) {
logSecurityEvent('failed_login', {
email: req.body.email,
ip: req.ip
});
return res.status(401).json({ error: 'Invalid credentials' });
}
// Success
});
✓ Monitor for Anomalies
Track: - Failed authentication attempts - Rate limit violations - Unusual access patterns - Privilege escalation attempts
✓ Implement Audit Logs
async function auditLog(action, userId, resource, details) {
await db.auditLogs.create({
timestamp: new Date(),
action,
userId,
resource,
details,
ip: req.ip
});
}
app.delete('/v1/pets/:id', async (req, res) => {
await deletePet(req.params.id);
await auditLog('delete_pet', req.auth.userId, `pet:${req.params.id}`, {
petName: pet.name
});
res.status(204).send();
});
Dependencies & Updates
✓ Keep Dependencies Updated
npm audit
npm audit fix
✓ Use Dependency Scanning
npm install -g snyk
snyk test
✓ Pin Dependency Versions
{
"dependencies": {
"express": "4.18.2", // Exact version, not "^4.18.2"
}
}
Testing
✓ Test Security Controls
describe('Authorization', () => {
it('should deny access to other users pets', async () => {
const response = await request(app)
.get('/v1/pets/123')
.set('Authorization', `Bearer ${otherUserToken}`);
expect(response.status).toBe(403);
});
it('should require authentication', async () => {
const response = await request(app)
.get('/v1/pets/123');
expect(response.status).toBe(401);
});
});
✓ Perform Penetration Testing
Regularly test for: - SQL injection - XSS - CSRF - Authentication bypass - Authorization bypass
Deployment
✓ Use Environment Variables
// ❌ Don't hardcode secrets
const JWT_SECRET = 'my-secret-key';
// ✓ Use environment variables
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is required');
}
✓ Disable Debug Mode in Production
if (process.env.NODE_ENV === 'production') {
app.set('env', 'production');
app.disable('x-powered-by');
}
✓ Use a WAF (Web Application Firewall)
Deploy behind a WAF like: - AWS WAF - Cloudflare - Akamai
Security Checklist Summary
Authentication & Authorization- [ ] HTTPS everywhere - [ ] Proper authentication method - [ ] Object-level authorization - [ ] Function-level authorization - [ ] Short-lived tokens
Input Validation- [ ] Validate all input - [ ] Sanitize input - [ ] Prevent SQL injection - [ ] Prevent NoSQL injection - [ ] Limit request size
Rate Limiting- [ ] Global rate limits - [ ] Endpoint-specific limits - [ ] Rate limit headers
Data Protection- [ ] No sensitive data in logs - [ ] Hash passwords - [ ] Encrypt sensitive data - [ ] Filter response fields
Error Handling- [ ] No stack traces exposed - [ ] Standard error format
Headers & CORS- [ ] Security headers - [ ] Proper CORS configuration
Monitoring- [ ] Log security events - [ ] Monitor anomalies - [ ] Audit logs
Dependencies- [ ] Keep dependencies updated - [ ] Dependency scanning - [ ] Pin versions
Testing- [ ] Security tests - [ ] Penetration testing
Deployment- [ ] Environment variables - [ ] Disable debug mode - [ ] Use WAF
Security is ongoing. Review this checklist regularly and stay updated on new vulnerabilities.
Related Articles: - OWASP Top 10 API Security- API Keys vs OAuth vs JWT- CORS Explained