REST API Security Code Review Guide
Table of Contents
Introduction
REST APIs have become the backbone of modern web and mobile applications. They power everything from social media platforms to banking systems. However, this ubiquity makes them prime targets for attackers. API security vulnerabilities can expose sensitive data, enable unauthorized actions, and compromise entire systems.
Unlike traditional web applications where server-side rendering provides some inherent protection, APIs expose raw data and functionality directly. This means every endpoint is a potential attack surface that must be explicitly secured. The OWASP API Security Top 10 highlights that API-specific vulnerabilities differ significantly from traditional web app vulnerabilities.
API Breaches Are Costly
According to recent studies, API-related breaches account for over 90% of web application attacks. Major breaches at companies like Facebook, Uber, and T-Mobile were all caused by API security failures. The average cost of an API breach exceeds $6 million.
Why are APIs particularly attractive targets for attackers?
API Security Layers
Effective API security requires defense in depth - multiple layers of protection that work together. If one layer fails, others should still protect the system.
REST API Security Layers
HTTPS, TLS 1.3JWT, OAuth 2.0, API KeysRBAC, ABAC, ScopesSchema validation, SanitizationEncryption, Masking, FilteringThrottling, Quotas, DDoS protectionOWASP API Security Top 10 (2023)
| Rank | Vulnerability | Description |
|---|---|---|
| API1 | Broken Object Level Authorization | Accessing objects belonging to other users |
| API2 | Broken Authentication | Weak or missing authentication mechanisms |
| API3 | Broken Object Property Level Auth | Exposing or modifying sensitive properties |
| API4 | Unrestricted Resource Consumption | No limits on requests or data size |
| API5 | Broken Function Level Authorization | Accessing admin functions as regular user |
| API6 | Unrestricted Access to Sensitive Flows | Automating sensitive operations |
| API7 | Server Side Request Forgery | Making server request attacker-controlled URLs |
| API8 | Security Misconfiguration | Improper security settings |
| API9 | Improper Inventory Management | Undocumented or deprecated endpoints |
| API10 | Unsafe Consumption of APIs | Trusting third-party API responses |
Defense in Depth
No single security control is sufficient. Implement multiple layers so that a failure in one does not compromise the entire system. Authentication protects who can access, authorization protects what they can access, and input validation protects how they access it.
Authentication Flaws
API authentication vulnerabilities allow attackers to impersonate legitimate users or bypass authentication entirely. Common flaws include weak credentials, insecure token handling, and missing authentication on sensitive endpoints.
Common Authentication Vulnerabilities
1// VULNERABLE: No authentication on sensitive endpoint
2app.get('/api/admin/users', async (req, res) => {
3 const users = await User.find({});
4 res.json(users); // Anyone can access all users!
5});
6
7// VULNERABLE: Authentication bypass via parameter
8app.get('/api/user/:id', async (req, res) => {
9 if (req.query.admin === 'true') {
10 // Bypassed with ?admin=true
11 return res.json(await User.findById(req.params.id));
12 }
13 // ... normal auth check
14});
15
16// VULNERABLE: Weak API key validation
17app.use((req, res, next) => {
18 const apiKey = req.headers['x-api-key'];
19 if (apiKey) {
20 next(); // Any non-empty key is accepted!
21 } else {
22 res.status(401).json({ error: 'Unauthorized' });
23 }
24});
25
26// VULNERABLE: Hardcoded credentials
27const ADMIN_TOKEN = 'admin123'; // In source code!
28if (req.headers.authorization === ADMIN_TOKEN) {
29 // Grant admin access
30}Secure Authentication Patterns
1const jwt = require('jsonwebtoken');
2
3// Middleware to verify JWT
4const authenticate = async (req, res, next) => {
5 try {
6 const authHeader = req.headers.authorization;
7 if (!authHeader?.startsWith('Bearer ')) {
8 return res.status(401).json({ error: 'Missing token' });
9 }
10
11 const token = authHeader.substring(7);
12 const decoded = jwt.verify(token, process.env.JWT_SECRET, {
13 algorithms: ['HS256'],
14 issuer: 'https://api.myapp.com'
15 });
16
17 // Attach user to request
18 req.user = await User.findById(decoded.sub);
19 if (!req.user) {
20 return res.status(401).json({ error: 'User not found' });
21 }
22
23 next();
24 } catch (error) {
25 return res.status(401).json({ error: 'Invalid token' });
26 }
27};
28
29// Apply to all routes that need authentication
30app.use('/api/protected/*', authenticate);
31
32// API key validation with proper lookup
33const validateApiKey = async (req, res, next) => {
34 const apiKey = req.headers['x-api-key'];
35 if (!apiKey) {
36 return res.status(401).json({ error: 'API key required' });
37 }
38
39 // Hash and compare (never store plain API keys)
40 const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
41 const keyRecord = await ApiKey.findOne({ hashedKey, active: true });
42
43 if (!keyRecord) {
44 return res.status(401).json({ error: 'Invalid API key' });
45 }
46
47 req.apiClient = keyRecord.client;
48 next();
49};What's wrong with checking if(apiKey) to validate API keys?
Input Validation
APIs must validate all input data - not just for security, but also for data integrity. Without proper validation, APIs are vulnerable to injection attacks, data corruption, and crashes.
Input Validation Vulnerabilities
1// VULNERABLE: No validation - SQL Injection
2app.get('/api/users/search', async (req, res) => {
3 const query = "SELECT * FROM users WHERE name = '" + req.query.name + "'";
4 // Injection: ?name=' OR '1'='1
5});
6
7// VULNERABLE: No validation - NoSQL Injection
8app.post('/api/login', async (req, res) => {
9 const user = await User.findOne({
10 email: req.body.email,
11 password: req.body.password // { "$ne": "" } bypasses auth!
12 });
13});
14
15// VULNERABLE: No type checking
16app.post('/api/products', async (req, res) => {
17 const product = new Product({
18 name: req.body.name,
19 price: req.body.price, // Could be negative, string, or object!
20 quantity: req.body.quantity
21 });
22});
23
24// VULNERABLE: Path traversal
25app.get('/api/files/:filename', (req, res) => {
26 const filePath = './uploads/' + req.params.filename;
27 // Attacker: /api/files/../../etc/passwd
28 res.sendFile(filePath);
29});Secure Input Validation
1const Joi = require('joi');
2
3// Define schemas for all inputs
4const schemas = {
5 createProduct: Joi.object({
6 name: Joi.string().min(1).max(100).required(),
7 price: Joi.number().positive().precision(2).required(),
8 quantity: Joi.number().integer().min(0).required(),
9 category: Joi.string().valid('electronics', 'clothing', 'food').required()
10 }),
11
12 searchUsers: Joi.object({
13 name: Joi.string().max(50).pattern(/^[a-zA-Z ]+$/),
14 page: Joi.number().integer().min(1).default(1),
15 limit: Joi.number().integer().min(1).max(100).default(20)
16 })
17};
18
19// Validation middleware
20const validate = (schemaName, source = 'body') => (req, res, next) => {
21 const { error, value } = schemas[schemaName].validate(req[source], {
22 abortEarly: false,
23 stripUnknown: true // Remove unexpected fields
24 });
25
26 if (error) {
27 return res.status(400).json({
28 error: 'Validation failed',
29 details: error.details.map(d => d.message)
30 });
31 }
32
33 req.validated = value;
34 next();
35};
36
37// Usage
38app.post('/api/products',
39 authenticate,
40 validate('createProduct', 'body'),
41 async (req, res) => {
42 // req.validated contains sanitized data
43 const product = new Product(req.validated);
44 await product.save();
45 res.status(201).json(product);
46 }
47);
48
49// Prevent path traversal
50const path = require('path');
51app.get('/api/files/:filename', authenticate, (req, res) => {
52 const uploadsDir = path.resolve('./uploads');
53 const filePath = path.resolve(uploadsDir, req.params.filename);
54
55 // Ensure the resolved path is within uploads directory
56 if (!filePath.startsWith(uploadsDir)) {
57 return res.status(400).json({ error: 'Invalid filename' });
58 }
59
60 res.sendFile(filePath);
61});What is the purpose of 'stripUnknown: true' in Joi validation?
Excessive Data Exposure
APIs often return more data than necessary, relying on clients to filter sensitive information. This 'excessive data exposure' violates the principle of least privilege and can leak sensitive information.
Data Exposure Vulnerabilities
1// VULNERABLE: Returns entire user object
2app.get('/api/users/:id', authenticate, async (req, res) => {
3 const user = await User.findById(req.params.id);
4 res.json(user); // Includes password hash, SSN, internal flags!
5});
6
7// Response includes sensitive data:
8{
9 "id": "123",
10 "name": "John Doe",
11 "email": "john@example.com",
12 "passwordHash": "$2b$10$...", // LEAKED!
13 "ssn": "123-45-6789", // LEAKED!
14 "internalNotes": "VIP customer", // LEAKED!
15 "failedLoginAttempts": 3, // LEAKED!
16 "roles": ["admin"] // LEAKED!
17}
18
19// VULNERABLE: Debug information in errors
20app.use((err, req, res, next) => {
21 res.status(500).json({
22 error: err.message,
23 stack: err.stack, // Reveals internal code paths!
24 query: req.query, // Reveals request data!
25 dbError: err.originalError // Reveals DB details!
26 });
27});
28
29// VULNERABLE: Exposing internal IDs
30app.get('/api/orders', authenticate, async (req, res) => {
31 const orders = await Order.find({ userId: req.user.id });
32 // Response includes: "_id", "__v", "userId", "internalStatus"
33});Secure Data Response Patterns
1// Define explicit response schemas (DTOs)
2const userPublicFields = ['id', 'name', 'email', 'avatar', 'createdAt'];
3
4const sanitizeUser = (user) => {
5 const sanitized = {};
6 for (const field of userPublicFields) {
7 sanitized[field] = user[field];
8 }
9 return sanitized;
10};
11
12app.get('/api/users/:id', authenticate, async (req, res) => {
13 const user = await User.findById(req.params.id);
14 res.json(sanitizeUser(user)); // Only public fields
15});
16
17// Using select() to limit database queries
18app.get('/api/users', authenticate, async (req, res) => {
19 const users = await User.find({})
20 .select('id name email avatar createdAt') // Only fetch needed fields
21 .lean(); // Plain objects, no Mongoose overhead
22 res.json(users);
23});
24
25// Transform output with toJSON
26const userSchema = new mongoose.Schema({
27 name: String,
28 email: String,
29 passwordHash: String,
30 ssn: String
31});
32
33userSchema.set('toJSON', {
34 transform: (doc, ret) => {
35 delete ret.passwordHash;
36 delete ret.ssn;
37 delete ret.__v;
38 ret.id = ret._id;
39 delete ret._id;
40 return ret;
41 }
42});
43
44// Safe error handling
45app.use((err, req, res, next) => {
46 // Log full error internally
47 console.error('API Error:', {
48 error: err,
49 path: req.path,
50 userId: req.user?.id
51 });
52
53 // Return sanitized error to client
54 const statusCode = err.statusCode || 500;
55 res.status(statusCode).json({
56 error: statusCode === 500 ? 'Internal server error' : err.message,
57 code: err.code || 'UNKNOWN_ERROR'
58 });
59});Why should APIs avoid returning entire database objects?
Prevention Techniques
Securing REST APIs requires a combination of authentication, authorization, input validation, and output filtering. Here are essential security controls for API development.
API Security Checklist
| Category | Control | Priority |
|---|---|---|
| Authentication | Use OAuth 2.0 or JWT with proper validation | Critical |
| Authentication | Implement token expiration and rotation | High |
| Authentication | Use secure password hashing (bcrypt, Argon2) | Critical |
| Authorization | Verify resource ownership on every request | Critical |
| Authorization | Implement role-based access control (RBAC) | High |
| Authorization | Use UUIDs instead of sequential IDs | Medium |
| Input | Validate all inputs with schema validation | Critical |
| Input | Use parameterized queries for databases | Critical |
| Input | Limit request body size | High |
| Output | Define explicit response schemas (DTOs) | High |
| Output | Never expose stack traces in production | Critical |
| Transport | Enforce HTTPS everywhere | Critical |
| Transport | Set security headers (CORS, CSP) | High |
| Monitoring | Log authentication failures | High |
| Monitoring | Implement anomaly detection | Medium |
Security Headers for APIs
1const helmet = require('helmet');
2const cors = require('cors');
3
4// Security headers
5app.use(helmet());
6
7// Strict CORS configuration
8app.use(cors({
9 origin: ['https://myapp.com', 'https://admin.myapp.com'],
10 methods: ['GET', 'POST', 'PUT', 'DELETE'],
11 allowedHeaders: ['Content-Type', 'Authorization'],
12 credentials: true,
13 maxAge: 86400 // Cache preflight for 24 hours
14}));
15
16// Additional security headers
17app.use((req, res, next) => {
18 // Prevent caching of API responses with sensitive data
19 res.set('Cache-Control', 'no-store');
20 res.set('Pragma', 'no-cache');
21
22 // Prevent MIME type sniffing
23 res.set('X-Content-Type-Options', 'nosniff');
24
25 // Remove server fingerprint
26 res.removeHeader('X-Powered-By');
27
28 next();
29});
30
31// Request size limits
32app.use(express.json({ limit: '100kb' }));
33app.use(express.urlencoded({ limit: '100kb', extended: true }));Why is strict CORS configuration important for APIs?