API security is critical. A single vulnerability can expose user data, allow unauthorized access, or bring down your entire system. After building APIs for multiple production applications, I've seen the most common security issues and how to fix them.
The Most Critical API Security Vulnerabilities
1. Broken Authentication and Authorization
The Problem:
Weak authentication allows attackers to access user accounts or admin functions.
Common Mistakes:
// BAD - No authentication check
app.get('/api/users', (req, res) => {
const users = db.getUsers();
res.json(users); // Anyone can access!
});
// BAD - Weak password validation
if (password.length > 6) {
// Too weak!
}
// BAD - No token expiration
const token = jwt.sign({ userId }, secret); // Never expires!The Fix:
// GOOD - Proper authentication middleware
const authenticateToken = (req, res, next) => {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access denied' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid token' });
}
};
// Protect routes
app.get('/api/users', authenticateToken, (req, res) => {
// Only authenticated users can access
const users = db.getUsers();
res.json(users);
});
// Strong password validation
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])[A-Za-zd@$!%*?&]{8,}$/;
if (!passwordRegex.test(password)) {
return res.status(400).json({
error: 'Password must be at least 8 characters with uppercase, lowercase, number, and special character'
});
}
// Token with expiration
const accessToken = jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' } // Short expiration
);
const refreshToken = jwt.sign(
{ userId },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);2. SQL Injection
The Problem:
SQL injection allows attackers to execute malicious SQL queries through your API.
Common Mistakes:
// BAD - SQL injection vulnerability
app.get('/api/users', (req, res) => {
const query = `SELECT * FROM users WHERE id = ${req.query.id}`;
db.query(query, (err, results) => {
res.json(results);
});
});
// If user sends: ?id=1 OR 1=1
// Query becomes: SELECT * FROM users WHERE id = 1 OR 1=1
// Returns all users!The Fix:
// GOOD - Parameterized queries
app.get('/api/users', (req, res) => {
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [req.query.id], (err, results) => {
res.json(results);
});
});
// With ORMs (Mongoose, Sequelize, Prisma)
// They handle parameterization automatically
const user = await User.findById(req.query.id); // Safe!
const user = await User.findOne({
where: { id: req.query.id }
}); // Safe!3. No Rate Limiting
The Problem:
Without rate limiting, attackers can overwhelm your API with requests, causing DoS (Denial of Service).
The Fix:
// Install express-rate-limit
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// General rate limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.'
});
app.use('/api/', limiter);
// Stricter limiter for auth routes
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Only 5 login attempts per 15 minutes
skipSuccessfulRequests: true
});
app.use('/api/auth/login', authLimiter);4. Exposed Sensitive Data
The Problem:
APIs often return more data than necessary, exposing sensitive information.
Common Mistakes:
// BAD - Returns password hash and internal IDs
app.get('/api/users/:id', (req, res) => {
const user = db.getUser(req.params.id);
res.json(user); // Includes password hash, internal IDs, etc.
});The Fix:
// GOOD - Return only necessary data
app.get('/api/users/:id', (req, res) => {
const user = db.getUser(req.params.id);
// Remove sensitive fields
const { password, internalId, ...safeUser } = user;
res.json(safeUser);
});
// Or use projection
const user = await User.findById(req.params.id)
.select('-password -internalId -__v');
res.json(user);5. Missing Input Validation
The Problem:
Unvalidated input can cause errors, crashes, or security vulnerabilities.
Common Mistakes:
// BAD - No validation
app.post('/api/users', (req, res) => {
const { email, age } = req.body;
db.createUser({ email, age }); // What if email is invalid? What if age is negative?
});The Fix:
// GOOD - Validate all input
const { body, validationResult } = require('express-validator');
app.post('/api/users',
[
body('email').isEmail().normalizeEmail(),
body('age').isInt({ min: 0, max: 120 }),
body('name').trim().isLength({ min: 2, max: 50 }),
body('password').isLength({ min: 8 }).matches(/[A-Z]/).matches(/[0-9]/)
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, age, name, password } = req.body;
db.createUser({ email, age, name, password });
res.status(201).json({ message: 'User created' });
}
);6. Insecure Direct Object References (IDOR)
The Problem:
APIs that use predictable IDs allow users to access other users' data.
Common Mistakes:
// BAD - No authorization check
app.get('/api/users/:id', (req, res) => {
const user = db.getUser(req.params.id);
res.json(user); // User can access any user's data by changing ID
});The Fix:
// GOOD - Check authorization
app.get('/api/users/:id', authenticateToken, (req, res) => {
// Users can only access their own data
if (req.user.userId !== req.params.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
const user = db.getUser(req.params.id);
res.json(user);
});7. Missing HTTPS
The Problem:
APIs without HTTPS expose data in transit.
The Fix:
// Always use HTTPS in production
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
// Use Helmet for security headers
const helmet = require('helmet');
app.use(helmet());8. Insufficient Logging and Monitoring
The Problem:
Without logging, you won't know when attacks happen.
The Fix:
// Log important events
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 authentication attempts
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
logger.info('Login attempt', {
email,
ip: req.ip,
userAgent: req.get('user-agent')
});
// ... authentication logic
if (failed) {
logger.warn('Failed login attempt', { email, ip: req.ip });
}
});
// Log API errors
app.use((err, req, res, next) => {
logger.error('API Error', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip
});
res.status(500).json({ error: 'Internal server error' });
});API Security Checklist
1. ✅ Implement proper authentication (JWT with expiration)
2. ✅ Use parameterized queries (prevent SQL injection)
3. ✅ Add rate limiting (prevent DoS attacks)
4. ✅ Validate all input (prevent invalid data)
5. ✅ Return only necessary data (don't expose sensitive info)
6. ✅ Check authorization (users can only access their data)
7. ✅ Use HTTPS (encrypt data in transit)
8. ✅ Add logging (monitor for attacks)
9. ✅ Use security headers (Helmet.js)
10. ✅ Keep dependencies updated (npm audit)
Security Headers with Helmet
const helmet = require('helmet');
app.use(helmet());
// Helmet sets these headers:
// - Content-Security-Policy
// - X-Content-Type-Options
// - X-Frame-Options
// - X-XSS-Protection
// - Strict-Transport-SecurityTesting API Security
Use tools:
- OWASP ZAP (penetration testing)
- Postman (API testing)
- Burp Suite (security testing)
- npm audit (dependency vulnerabilities)
Manual testing:
- Try accessing protected routes without auth
- Test SQL injection with malicious input
- Test rate limiting by sending many requests
- Check if sensitive data is exposed
Conclusion
API security is not optional. These vulnerabilities are real and can have serious consequences:
- **Data breaches** - Exposed user information
- **Financial loss** - Stolen payment information
- **Reputation damage** - Loss of user trust
- **Legal issues** - GDPR violations, lawsuits
Implement these security measures from the start. It's much easier to build secure APIs than to fix security issues later.
Remember: Security is an ongoing process, not a one-time setup. Keep your dependencies updated, monitor your APIs, and stay informed about new vulnerabilities.