- name
- api-security-checklist
- description
- Comprehensive API security checklist and best practices for designing, testing, and securing REST, GraphQL, and OAuth APIs
- triggers
- ["show me API security best practices","how do I secure my REST API","what security checks should I implement for my API","help me review API security","OAuth security recommendations","API authentication and authorization checklist","protect my API from attacks","API security audit checklist"]
# API Security Checklist Skill
> Skill by [ara.so](https://ara.so) — Security Skills collection.
This skill provides comprehensive guidance on API security best practices based on the widely-adopted API Security Checklist. Use this to design, audit, and secure REST, GraphQL, and OAuth APIs against common vulnerabilities and attack vectors.
## Overview
The API Security Checklist covers critical security countermeasures across:
- **Authentication** - Secure user identity verification
- **Authorization** - Access control and OAuth flows
- **Input Validation** - Preventing injection attacks
- **Output Security** - Secure response handling
- **Processing** - Backend security measures
- **Monitoring** - Detection and alerting
- **CI/CD** - Secure development lifecycle
## Installation
This is a knowledge resource, not a software package. To use:
1. **Bookmark for reference**: Keep the checklist accessible during API development
2. **Integrate into code reviews**: Use as a PR checklist template
3. **Add to CI/CD**: Implement automated checks based on these guidelines
4. **Security audits**: Use as an audit framework
## Authentication Security
### ❌ Avoid Basic Auth
```javascript
// BAD - Basic Auth is insecure
app.get('/api/users', (req, res) => {
const auth = req.headers.authorization;
const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
// Don't do this!
});
```
### ✅ Use Standard Authentication
```javascript
// GOOD - JWT with proper validation
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.get('/api/users', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
```
### Rate Limiting and Max Retry
```javascript
const rateLimit = require('express-rate-limit');
// Limit login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
app.post('/api/login', loginLimiter, async (req, res) => {
// Login logic
});
```
### Password Storage
```javascript
const bcrypt = require('bcrypt');
// GOOD - Hash passwords with bcrypt
async function hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// Usage
app.post('/api/register', async (req, res) => {
const { email, password } = req.body;
const hashedPassword = await hashPassword(password);
// Store hashedPassword in database
});
```
## Access Control
### HTTPS and Security Headers
```javascript
const helmet = require('helmet');
const express = require('express');
const app = express();
// Use Helmet for security headers
app.use(helmet({
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none'"]
}
},
frameguard: { action: 'deny' },
noSniff: true
}));
// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect('https://' + req.headers.host + req.url);
}
next();
});
}
```
### IP Whitelisting for Private APIs
```javascript
const ipWhitelist = process.env.ALLOWED_IPS?.split(',') || [];
function checkIPWhitelist(req, res, next) {
const clientIP = req.ip || req.connection.remoteAddress;
if (!ipWhitelist.includes(clientIP)) {
return res.status(403).json({ error: 'IP not authorized' });
}
next();
}
app.use('/api/admin', checkIPWhitelist);
```
### DDoS Protection
```javascript
const rateLimit = require('express-rate-limit');
// General API rate limiting
const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Too many requests from this IP'
});
app.use('/api/', apiLimiter);
```
## OAuth Security
### Validate Redirect URI
```python
from urllib.parse import urlparse
ALLOWED_REDIRECT_URIS = [
'https://app.example.com/callback',
'https://app.example.com/oauth/callback'
]
def validate_redirect_uri(redirect_uri):
"""Always validate redirect_uri server-side"""
if redirect_uri not in ALLOWED_REDIRECT_URIS:
raise ValueError('Invalid redirect_uri')
return True
# In OAuth authorization endpoint
@app.route('/oauth/authorize')
def authorize():
redirect_uri = request.args.get('redirect_uri')
try:
validate_redirect_uri(redirect_uri)
except ValueError:
return {'error': 'invalid_redirect_uri'}, 400
# Continue with authorization
```
### Use State Parameter for CSRF Protection
```javascript
const crypto = require('crypto');
// Generate state parameter
function generateState() {
return crypto.randomBytes(32).toString('hex');
}
// OAuth authorization request
app.get('/oauth/login', (req, res) => {
const state = generateState();
// Store state in session
req.session.oauthState = state;
const authUrl = `https://provider.com/oauth/authorize?` +
`client_id=${process.env.OAUTH_CLIENT_ID}` +
`&redirect_uri=${encodeURIComponent(process.env.OAUTH_REDIRECT_URI)}` +
`&response_type=code` +
`&state=${state}` +
`&scope=read`;
res.redirect(authUrl);
});
// OAuth callback - validate state
app.get('/oauth/callback', (req, res) => {
const { code, state } = req.query;
// Validate state parameter
if (state !== req.session.oauthState) {
return res.status(403).json({ error: 'Invalid state parameter' });
}
// Exchange code for token
// Never use response_type=token (implicit flow)
});
```
### Scope Validation
```javascript
const VALID_SCOPES = ['read', 'write', 'admin'];
const DEFAULT_SCOPE = 'read';
function validateScopes(requestedScopes) {
if (!requestedScopes) return [DEFAULT_SCOPE];
const scopes = requestedScopes.split(' ');
const validScopes = scopes.filter(scope => VALID_SCOPES.includes(scope));
return validScopes.length > 0 ? validScopes : [DEFAULT_SCOPE];
}
app.post('/oauth/token', (req, res) => {
const requestedScopes = req.body.scope;
const allowedScopes = validateScopes(requestedScopes);
// Generate token with validated scopes only
const token = jwt.sign(
{ scopes: allowedScopes },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ access_token: token, scope: allowedScopes.join(' ') });
});
```
## Input Validation
### HTTP Method Validation
```javascript
const ALLOWED_METHODS = {
'/api/users': ['GET', 'POST'],
'/api/users/:id': ['GET', 'PUT', 'PATCH', 'DELETE']
};
function validateMethod(req, res, next) {
const allowedForRoute = ALLOWED_METHODS[req.route.path];
if (!allowedForRoute || !allowedForRoute.includes(req.method)) {
res.set('Allow', allowedForRoute.join(', '));
return res.status(405).json({ error: 'Method Not Allowed' });
}
next();
}
app.use(validateMethod);
```
### Content-Type Validation
```javascript
const SUPPORTED_CONTENT_TYPES = [
'application/json',
'application/xml'
];
function validateContentType(req, res, next) {
// Validate Accept header
const accept = req.headers.accept;
const acceptsSupported = SUPPORTED_CONTENT_TYPES.some(type =>
accept?.includes(type)
);
if (!acceptsSupported && accept !== '*/*') {
return res.status(406).json({ error: 'Not Acceptable' });
}
// Validate Content-Type for POST/PUT/PATCH
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
const contentType = req.headers['content-type']?.split(';')[0];
if (!SUPPORTED_CONTENT_TYPES.includes(contentType)) {
return res.status(415).json({ error: 'Unsupported Media Type' });
}
}
next();
}
app.use(validateContentType);
```
### Input Sanitization
```javascript
const validator = require('validator');
// Prevent XSS, SQL Injection, etc.
function sanitizeInput(data) {
if (typeof data === 'string') {
return validator.escape(data);
}
if (Array.isArray(data)) {
return data.map(sanitizeInput);
}
if (typeof data === 'object' && data !== null) {
const sanitized = {};
for (const [key, value] of Object.entries(data)) {
sanitized[key] = sanitizeInput(value);
}
return sanitized;
}
return data;
}
app.post('/api/users', (req, res) => {
const sanitizedBody = sanitizeInput(req.body);
// Use sanitizedBody instead of req.body
});
```
### Prevent XXE (XML External Entity)
```javascript
const libxmljs = require('libxmljs');
function parseXMLSafely(xmlString) {
try {
// Disable external entity parsing
const doc = libxmljs.parseXml(xmlString, {
noent: false, // Don't substitute entities
nonet: true, // Don't access network
dtdload: false // Don't load external DTDs
});
return doc;
} catch (error) {
throw new Error('Invalid XML');
}
}
app.post('/api/data', (req, res) => {
if (req.headers['content-type'] === 'application/xml') {
try {
const doc = parseXMLSafely(req.body);
// Process document
} catch (error) {
return res.status(400).json({ error: 'Invalid XML' });
}
}
});
```
## Processing Security
### Avoid Auto-Increment IDs (Use UUIDs)
```javascript
const { v4: uuidv4 } = require('uuid');
// GOOD - Use UUIDs instead of auto-increment IDs
app.post('/api/users', async (req, res) => {
const user = {
id: uuidv4(), // e.g., '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
...req.body
};
await db.users.create(user);
res.json(user);
});
```
### Use /me for User Resources
```javascript
// BAD - Exposes user IDs
GitHub에서 보기