api-security-best-practices について
API Security Best Practicesは、REST、GraphQL、WebSocket APIを保護するための開発者向けガイドです。認証、入力処理、不正利用対策、データ保護の実装パターンとレビュー用チェックリストを扱います。
主な機能
- 01認証と認可のパターン
- 02入力検証とサニタイズ
- 03レート制限とスロットリングの指針
- 04機密データ保護の実践
- 05APIセキュリティテストのチェックリスト
活用シーン
- 01新しいAPIエンドポイントのセキュリティ設計
- 02既存APIの一般的な脆弱性レビュー
- 03APIセキュリティ監査項目の準備
- 04レート制限と入力検証の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ davila7/ claude-code-templates --skill api-security-best-practicesインストール後、Claude Code で/api-security-best-practicesを入力して読み込まれたか確認します。
詳細
API セキュリティのベストプラクティス
概要
認証、認可、入力検証、レート制限、一般的な脆弱性からの保護を実装することで、開発者が安全な API を構築できるよう支援します。このスキルは、REST、GraphQL、WebSocket API のセキュリティパターンを扱います。
このスキルを使用するタイミング
- 新しい API エンドポイントを設計するとき
- 既存の API を保護するとき
- 認証と認可を実装するとき
- API への攻撃(インジェクション、DDoS など)から保護するとき
- API のセキュリティレビューを実施するとき
- セキュリティ監査に備えるとき
- レート制限とスロットリングを実装するとき
- API で機密データを扱うとき
仕組み
ステップ 1: 認証と認可
安全な認証の実装をお手伝いします。
- 認証方式(JWT、OAuth 2.0、API キー)を選択する
- トークンベースの認証を実装する
- ロールベースアクセス制御(RBAC)を設定する
- セッション管理を安全にする
- 多要素認証(MFA)を実装する
ステップ 2: 入力検証とサニタイズ
インジェクション攻撃から保護します。
- すべての入力データを検証する
- ユーザー入力をサニタイズする
- パラメータ化クエリを使用する
- リクエストのスキーマ検証を実装する
- SQL インジェクション、XSS、コマンドインジェクションを防ぐ
ステップ 3: レート制限とスロットリング
悪用や DDoS 攻撃を防ぎます。
- ユーザーごと / IP ごとにレート制限を実装する
- API のスロットリングを設定する
- リクエストのクォータを構成する
- レート制限のエラーを適切に処理する
- 不審なアクティビティを監視する
ステップ 4: データ保護
機密データを保護します。
- 転送中のデータを暗号化する(HTTPS/TLS)
- 保存時の機密データを暗号化する
- 適切なエラー処理を実装する(データ漏えいを防ぐ)
- エラーメッセージをサニタイズする
- セキュリティヘッダーを使用する
ステップ 5: API セキュリティテスト
セキュリティの実装を検証します。
- 認証と認可をテストする
- ペネトレーションテストを実施する
- 一般的な脆弱性(OWASP API Top 10)を確認する
- 入力処理を検証する
- レート制限をテストする
例
例 1: JWT 認証の実装
## Secure JWT Authentication Implementation
### Authentication Flow
1. User logs in with credentials
2. Server validates credentials
3. Server generates JWT token
4. Client stores token securely
5. Client sends token with each request
6. Server validates token
### Implementation
#### 1. Generate Secure JWT Tokens
\`\`\`javascript
// auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({
error: 'Email and password are required'
});
}
// Find user
const user = await db.user.findUnique({
where: { email }
});
if (!user) {
// Don't reveal if user exists
return res.status(401).json({
error: 'Invalid credentials'
});
}
// Verify password
const validPassword = await bcrypt.compare(
password,
user.passwordHash
);
if (!validPassword) {
return res.status(401).json({
error: 'Invalid credentials'
});
}
// Generate JWT token
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{
expiresIn: '1h',
issuer: 'your-app',
audience: 'your-app-users'
}
);
// Generate refresh token
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token in database
await db.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
});
res.json({
token,
refreshToken,
expiresIn: 3600
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
error: 'An error occurred during login'
});
}
});
\`\`\`
#### 2. Verify JWT Tokens (Middleware)
\`\`\`javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
// Get token from header
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
error: 'Access token required'
});
}
// Verify token
jwt.verify(
token,
process.env.JWT_SECRET,
{
issuer: 'your-app',
audience: 'your-app-users'
},
(err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired'
});
}
return res.status(403).json({
error: 'Invalid token'
});
}
// Attach user to request
req.user = user;
next();
}
);
}
module.exports = { authenticateToken };
\`\`\`
#### 3. Protect Routes
\`\`\`javascript
const { authenticateToken } = require('./middleware/auth');
// Protected route
app.get('/api/user/profile', authenticateToken, async (req, res) => {
try {
const user = await db.user.findUnique({
where: { id: req.user.userId },
select: {
id: true,
email: true,
name: true,
// Don't return passwordHash
}
});
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
\`\`\`
#### 4. Implement Token Refresh
\`\`\`javascript
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
error: 'Refresh token required'
});
}
try {
// Verify refresh token
const decoded = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET
);
// Check if refresh token exists in database
const storedToken = await db.refreshToken.findFirst({
where: {
token: refreshToken,
userId: decoded.userId,
expiresAt: { gt: new Date() }
}
});
if (!storedToken) {
return res.status(403).json({
error: 'Invalid refresh token'
});
}
// Generate new access token
const user = await db.user.findUnique({
where: { id: decoded.userId }
});
const newToken = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({
token: newToken,
expiresIn: 3600
});
} catch (error) {
res.status(403).json({
error: 'Invalid refresh token'
});
}
});
\`\`\`
### Security Best Practices
- ✅ Use strong JWT secrets (256-bit minimum)
- ✅ Set short expiration times (1 hour for access tokens)
- ✅ Implement refresh tokens for long-lived sessions
- ✅ Store refresh tokens in database (can be revoked)
- ✅ Use HTTPS only
- ✅ Don't store sensitive data in JWT payload
- ✅ Validate token issuer and audience
- ✅ Implement token blacklisting for logout
例 2: 入力検証と SQL インジェクション対策
## Preventing SQL Injection and Input Validation
### The Problem
**❌ Vulnerable Code:**
\`\`\`javascript
// NEVER DO THIS - SQL Injection vulnerability
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
// Dangerous: User input directly in query
const query = \`SELECT * FROM users WHERE id = '\${userId}'\`;
const user = await db.query(query);
res.json(user);
});
// Attack example:
// GET /api/users/1' OR '1'='1
// Returns all users!
\`\`\`
### The Solution
#### 1. Use Parameterized Queries
\`\`\`javascript
// ✅ Safe: Parameterized query
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
// Validate input first
if (!userId || !/^\d+$/.test(userId)) {
return res.status(400).json({
error: 'Invalid user ID'
});
}
// Use parameterized query
const user = await db.query(
'SELECT id, email, name FROM users WHERE id = $1',
[userId]
);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json(user);
});
\`\`\`
#### 2. Use ORM with Proper Escaping
\`\`\`javascript
// ✅ Safe: Using Prisma ORM
app.get('/api/users/:id', async (req, res) => {
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
return res.status(400).json({
error: 'Invalid user ID'
});
}
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
// Don't select sensitive fields
}
});
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json(user);
});
\`\`\`
#### 3. Implement Request Validation with Zod
\`\`\`javascript
const { z } = require('zod');
// Define validation schema
const createUserSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain uppercase letter')
.regex(/[a-z]/, 'Password must contain lowercase letter')
.regex(/[0-9]/, 'Password must contain number'),
name: z.string()
.min(2, 'Name must be at least 2 characters')
.max(100, 'Name too long'),
age: z.number()
.int('Age must be an integer')
.min(18, 'Must be 18 or older')
.max(120, 'Invalid age')
.optional()
});
// Validation middleware
function validateRequest(schema) {
return (req, res, next) => {
try {
schema.parse(req.body);
next();
} catch (error) {
res.status(400).json({
error: 'Validation failed',
details: error.errors
});
}
};
}
// Use validation
app.post('/api/users',
validateRequest(createUserSchema),
async (req, res) => {
// Input is validated at this point
const { email, password, name, age } = req.body;
// Hash password
const passwordHash = await bcrypt.hash(password, 10);
// Create user
const user = await prisma.user.create({
data: {
email,
passwordHash,
name,
age
}
});
// Don't return password hash
const { passwordHash: _, ...userWithoutPassword } = user;
res.status(201).json(userWithoutPassword);
}
);
\`\`\`
#### 4. Sanitize Output to Prevent XSS
\`\`\`javascript
const DOMPurify = require('isomorphic-dompurify');
app.post('/api/comments', authenticateToken, async (req, res) => {
const { content } = req.body;
// Validate
if (!content || content.length > 1000) {
return res.status(400).json({
error: 'Invalid comment content'
});
}
// Sanitize HTML to prevent XSS
const sanitizedContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
const comment = await prisma.comment.create({
data: {
content: sanitizedContent,
userId: req.user.userId
}
});
res.status(201).json(comment);
});
\`\`\`
### Validation Checklist
- [ ] Validate all user inputs
- [ ] Use parameterized queries or ORM
- [ ] Validate data types (string, number, email, etc.)
- [ ] Validate data ranges (min/max length, value ranges)
- [ ] Sanitize HTML content
- [ ] Escape special characters
- [ ] Validate file uploads (type, size, content)
- [ ] Use allowlists, not blocklists
例 3: レート制限と DDoS 防御
## Implementing Rate Limiting
### Why Rate Limiting?
- Prevent brute force attacks
- Protect against DDoS
- Prevent API abuse
- Ensure fair usage
- Reduce server costs
### Implementation with Express Rate Limit
\`\`\`javascript
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
// Create Redis client
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
// General API rate limit
const apiLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:api:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: {
error: 'Too many requests, please try again later',
retryAfter: 900 // seconds
},
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
// Custom key generator (by user ID or IP)
keyGenerator: (req) => {
return req.user?.userId || req.ip;
}
});
// Strict rate limit for authentication endpoints
const authLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:auth:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Only 5 login attempts per 15 minutes
skipSuccessfulRequests: true, // Don't count successful logins
message: {
error: 'Too many login attempts, please try again later',
retryAfter: 900
}
});
// Apply rate limiters
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
// Custom rate limiter for expensive operations
const expensiveLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // 10 requests per hour
message: {
error: 'Rate limit exceeded for this operation'
}
});
app.post('/api/reports/generate',
authenticateToken,
expensiveLimiter,
async (req, res) => {
// Expensive operation
}
);
\`\`\`
### Advanced: Per-User Rate Limiting
\`\`\`javascript
// Different limits based on user tier
function createTieredRateLimiter() {
const limits = {
free: { windowMs: 60 * 60 * 1000, max: 100 },
pro: { windowMs: 60 * 60 * 1000, max: 1000 },
enterprise: { windowMs: 60 * 60 * 1000, max: 10000 }
};
return async (req, res, next) => {
const user = req.user;
const tier = user?.tier || 'free';
const limit = limits[tier];
const key = \`rl:user:\${user.userId}\`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, limit.windowMs / 1000);
}
if (current > limit.max) {
return res.status(429).json({
error: 'Rate limit exceeded',
limit: limit.max,
remaining: 0,
reset: await redis.ttl(key)
});
}
// Set rate limit headers
res.set({
'X-RateLimit-Limit': limit.max,
'X-RateLimit-Remaining': limit.max - current,
'X-RateLimit-Reset': await redis.ttl(key)
});
next();
};
}
app.use('/api/', authenticateToken, createTieredRateLimiter());
\`\`\`
### DDoS Protection with Helmet
\`\`\`javascript
const helmet = require('helmet');
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
// Prevent clickjacking
frameguard: { action: 'deny' },
// Hide X-Powered-By header
hidePoweredBy: true,
// Prevent MIME type sniffing
noSniff: true,
// Enable HSTS
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
\`\`\`
### Rate Limit Response Headers
\`\`\`
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1640000000
Retry-After: 900
\`\`\`
ベストプラクティス
✅ 推奨事項
- すべてで HTTPS を使用する - 機密データを HTTP で送信しない
- 認証を実装する - 保護されたエンドポイントには認証を必須にする
- すべての入力を検証する - ユーザー入力を決して信頼しない
- パラメータ化クエリを使用する - SQL インジェクションを防ぐ
- レート制限を実装する - ブルートフォース攻撃や DDoS から保護する
- パスワードをハッシュ化する - ソルトラウンド数 10 以上の bcrypt を使用する
- 短命のトークンを使用する - JWT のアクセストークンは速やかに失効させる
- CORS を適切に実装する - 信頼できるオリジンのみを許可する
- セキュリティイベントをログに記録する - 不審なアクティビティを監視する
- 依存関係を最新に保つ - パッケージを定期的に更新する
- セキュリティヘッダーを使用する - Helmet.js を実装する
- エラーメッセージをサニタイズする - 機密情報を漏らさない
❌ 避けるべきこと
- パスワードを平文で保存しない - パスワードは必ずハッシュ化する
- 弱いシークレットを使用しない - 強力でランダムな JWT シークレットを使用する
- ユーザー入力を信頼しない - 必ず検証とサニタイズを行う
- スタックトレースを公開しない - 本番環境ではエラーの詳細を隠す
- SQL に文字列連結を使用しない - パラメータ化クエリを使用する
- JWT に機密データを保存しない - JWT は暗号化されていない
- セキュリティアップデートを無視しない - 依存関係を定期的に更新する
- デフォルトの認証情報を使用しない - すべてのデフォルトパスワードを変更する
- CORS を完全に無効にしない - 代わりに適切に設定する
- 機密データをログに記録しない - ログをサニタイズする
よくある落とし穴
問題:JWT シークレットがコード内に露出している
症状: JWT シークレットがハードコードされている、または Git にコミットされている 解決策: ```javascript // ❌ 悪い例 const JWT_SECRET = 'my-secret-key';
// ✅ 良い例 const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { throw new Error('JWT_SECRET environment variable is required'); }
// 強力なシークレットを生成する // node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" ```
問題:パスワード要件が弱い
症状: ユーザーが「password123」のような弱いパスワードを設定できる 解決策: ```javascript const passwordSchema = z.string() .min(12, 'Password must be at least 12 characters') .regex(/[A-Z]/, 'Must contain uppercase letter') .regex(/[a-z]/, 'Must contain lowercase letter') .regex(/[0-9]/, 'Must contain number') .regex(/[^A-Za-z0-9]/, 'Must contain special character');
// または、パスワード強度ライブラリを使用する const zxcvbn = require('zxcvbn'); const result = zxcvbn(password); if (result.score < 3) { return res.status(400).json({ error: 'Password too weak', suggestions: result.feedback.suggestions }); } ```
問題:認可チェックがない
症状: ユーザーがアクセス権のないリソースにアクセスできる 解決策: ```javascript // ❌ 悪い例:認証のみをチェックする app.delete('/api/posts/:id', authenticateToken, async (req, res) => { await prisma.post.delete({ where: { id: req.params.id } }); res.json({ success: true }); });
// ✅ 良い例:認証と認可の両方をチェックする app.delete('/api/posts/:id', authenticateToken, async (req, res) => { const post = await prisma.post.findUnique({ where: { id: req.params.id } });
if (!post) { return res.status(404).json({ error: 'Post not found' }); }
// ユーザーが投稿の所有者または管理者かどうかを確認する if (post.userId !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Not authorized to delete this post' }); }
await prisma.post.delete({ where: { id: req.params.id } }); res.json({ success: true }); }); ```
問題:詳細すぎるエラーメッセージ
症状: エラーメッセージからシステムの詳細が漏れる
解決策:
```javascript
// ❌ 悪い例:データベースの詳細を露出する
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({ data: req.body });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
// エラー:「Unique constraint failed on the fields: (email)」
}
});
// ✅ 良い例:汎用的なエラーメッセージ app.post('/api/users', async (req, res) => { try { const user = await prisma.user.create({ data: req.body }); res.json(user); } catch (error) { console.error('User creation error:', error); // エラーの詳細をすべて記録する
if (error.code === 'P2002') {
return res.status(400).json({
error: 'Email already exists'
});
}
res.status(500).json({
error: 'An error occurred while creating user'
});
} }); ```
セキュリティチェックリスト
認証と認可
- 強力な認証を実装する(JWT、OAuth 2.0)
- すべてのエンドポイントで HTTPS を使用する
- bcrypt でパスワードをハッシュ化する(salt rounds >= 10)
- トークンの有効期限を実装する
- リフレッシュトークンの仕組みを追加する
- リクエストごとにユーザーの認可を確認する
- ロールベースのアクセス制御(RBAC)を実装する
入力の検証
- すべてのユーザー入力を検証する
- パラメーター化クエリまたは ORM を使用する
- HTML コンテンツをサニタイズする
- ファイルアップロードを検証する
- リクエストスキーマの検証を実装する
- ブロックリストではなく許可リストを使用する
レート制限と DDoS 対策
- ユーザー/IP ごとにレート制限を実装する
- 認証エンドポイントには、より厳しい制限を設定する
- 分散レート制限に Redis を使用する
- 適切なレート制限ヘッダーを返す
- リクエストのスロットリングを実装する
データ保護
- すべてのトラフィックにHTTPS/TLSを使用する
- 保存時の機密データを暗号化する
- 機密データをJWTに保存しない
- エラーメッセージをサニタイズする
- 適切なCORS設定を実装する
- セキュリティヘッダー(Helmet.js)を使用する
監視とロギング
- セキュリティイベントをログに記録する
- 不審なアクティビティを監視する
- 認証失敗時のアラートを設定する
- APIの使用パターンを追跡する
- 機密データをログに記録しない
OWASP API Security Top 10
- Broken Object Level Authorization - ユーザーがリソースにアクセスできるかを常に検証する
- Broken Authentication - 強力な認証メカニズムを実装する
- Broken Object Property Level Authorization - ユーザーがアクセスできるプロパティを検証する
- Unrestricted Resource Consumption - レート制限とクォータを実装する
- Broken Function Level Authorization - 各機能についてユーザーのロールを検証する
- Unrestricted Access to Sensitive Business Flows - 重要なワークフローを保護する
- Server Side Request Forgery (SSRF) - URLを検証しサニタイズする
- Security Misconfiguration - セキュリティのベストプラクティスとヘッダーを使用する
- Improper Inventory Management - すべてのAPIエンドポイントを文書化し、保護する
- Unsafe Consumption of APIs - サードパーティAPIから取得したデータを検証する
関連スキル
@ethical-hacking-methodology- セキュリティテストの観点@sql-injection-testing- SQLインジェクションのテスト@xss-html-injection- XSS脆弱性のテスト@broken-authentication- 認証の脆弱性@backend-dev-guidelines- バックエンド開発の標準@systematic-debugging- セキュリティ問題のデバッグ
追加リソース
- OWASP API Security Top 10
- JWT Best Practices
- Express Security Best Practices
- Node.js Security Checklist
- API Security Checklist
Pro Tip: セキュリティは一度きりの作業ではありません。APIを定期的に監査し、依存関係を最新の状態に保ち、新たな脆弱性について常に情報を収集してください。