APIセキュリティ - 安全なAPI設計と実装

18分 で読める | 2025.01.10

公式ドキュメント

OWASP API Security Top 10

順位脆弱性対策
1認可の不備(BOLA)オブジェクトレベルの権限チェック
2認証の不備強力な認証メカニズム
3オブジェクトプロパティレベルの認可不備レスポンスフィルタリング
4リソース消費の制限なしレート制限、ペイロードサイズ制限
5機能レベルの認可不備RBAC/ABAC実装
6サーバーサイドリクエストフォージェリURL検証、ホワイトリスト
7セキュリティ設定ミスセキュリティヘッダー設定
8インジェクション入力検証、パラメータ化クエリ
9不適切な資産管理APIインベントリ管理
10安全でないAPIの使用サードパーティAPI検証

認証・認可

JWT + リフレッシュトークン

import jwt from 'jsonwebtoken';

const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';

function generateTokens(userId: string, role: string) {
 const accessToken = jwt.sign(
 { sub: userId, role, type: 'access' },
 process.env.JWT_SECRET!,
 { expiresIn: ACCESS_TOKEN_EXPIRY }
 );

 const refreshToken = jwt.sign(
 { sub: userId, type: 'refresh' },
 process.env.JWT_REFRESH_SECRET!,
 { expiresIn: REFRESH_TOKEN_EXPIRY }
 );

 return { accessToken, refreshToken };
}

// ミドルウェア
function authMiddleware(req: Request, res: Response, next: NextFunction) {
 const token = req.headers.authorization?.replace('Bearer ', '');

 if (!token) {
 return res.status(401).json({ error: 'No token provided' });
 }

 try {
 const payload = jwt.verify(token, process.env.JWT_SECRET!, {
 algorithms: ['HS256'], // アルゴリズム明示指定
 });
 req.user = payload;
 next();
 } catch (error) {
 return res.status(401).json({ error: 'Invalid token' });
 }
}

RBAC(ロールベースアクセス制御)

const permissions = {
 admin: ['read', 'write', 'delete', 'manage_users'],
 editor: ['read', 'write'],
 viewer: ['read'],
};

function requirePermission(...requiredPermissions: string[]) {
 return (req: Request, res: Response, next: NextFunction) => {
 const userRole = req.user?.role;
 const userPermissions = permissions[userRole] || [];

 const hasPermission = requiredPermissions.every(
 p => userPermissions.includes(p)
 );

 if (!hasPermission) {
 return res.status(403).json({ error: 'Insufficient permissions' });
 }

 next();
 };
}

// 使用
app.delete('/users/:id', authMiddleware, requirePermission('delete', 'manage_users'), deleteUser);

ポイント: 認証(Authentication)は「誰であるか」、認可(Authorization)は「何ができるか」を確認するプロセスです。この2つは必ず分離して実装しましょう。

入力検証

import { z } from 'zod';

const CreateUserSchema = z.object({
 email: z.string().email().max(255),
 password: z.string().min(8).max(100)
 .regex(/[A-Z]/, 'Must contain uppercase')
 .regex(/[0-9]/, 'Must contain number'),
 name: z.string().min(1).max(100)
 .regex(/^[a-zA-Z\s]+$/, 'Only letters allowed'),
});

function validateBody<T>(schema: z.ZodSchema<T>) {
 return (req: Request, res: Response, next: NextFunction) => {
 const result = schema.safeParse(req.body);

 if (!result.success) {
 return res.status(400).json({
 error: 'Validation failed',
 details: result.error.flatten().fieldErrors,
 });
 }

 req.body = result.data;
 next();
 };
}

app.post('/users', validateBody(CreateUserSchema), createUser);

レート制限

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// 一般的なレート制限
const generalLimiter = rateLimit({
 store: new RedisStore({ client: redisClient }),
 windowMs: 15 * 60 * 1000, // 15分
 max: 100,
 message: { error: 'Too many requests' },
 standardHeaders: true,
});

// 認証エンドポイント用(より厳しい)
const authLimiter = rateLimit({
 store: new RedisStore({ client: redisClient }),
 windowMs: 60 * 60 * 1000, // 1時間
 max: 5, // 5回まで
 message: { error: 'Too many login attempts' },
});

app.use('/api', generalLimiter);
app.use('/api/auth/login', authLimiter);

セキュリティヘッダー

import helmet from 'helmet';

app.use(helmet({
 contentSecurityPolicy: {
 directives: {
 defaultSrc: ["'self'"],
 scriptSrc: ["'self'"],
 styleSrc: ["'self'", "'unsafe-inline'"],
 imgSrc: ["'self'", "data:", "https:"],
 },
 },
 hsts: {
 maxAge: 31536000,
 includeSubDomains: true,
 preload: true,
 },
}));

// CORS設定
app.use(cors({
 origin: ['https://example.com', 'https://app.example.com'],
 methods: ['GET', 'POST', 'PUT', 'DELETE'],
 allowedHeaders: ['Content-Type', 'Authorization'],
 credentials: true,
 maxAge: 86400,
}));

注意: SQLインジェクションは最も危険な脆弱性の1つです。ユーザー入力を直接SQL文に埋め込むことは絶対に避けてください。

SQLインジェクション対策

// 悪い例
const query = `SELECT * FROM users WHERE id = '${userId}'`;

// 良い例(パラメータ化クエリ)
const [users] = await db.query(
 'SELECT * FROM users WHERE id = ?',
 [userId]
);

// ORMを使用(さらに安全)
const user = await prisma.user.findUnique({
 where: { id: userId },
});

実践メモ: レスポンスには必要最小限のフィールドのみを含め、password等の機密データは必ず除外しましょう。

機密データの保護

// レスポンスから機密フィールドを除外
function sanitizeUser(user: User): PublicUser {
 const { password, refreshToken, ...publicData } = user;
 return publicData;
}

// シリアライザパターン
class UserSerializer {
 static toPublic(user: User): PublicUser {
 return {
 id: user.id,
 email: user.email,
 name: user.name,
 createdAt: user.createdAt,
 };
 }

 static toAdmin(user: User): AdminUser {
 return {
 ...this.toPublic(user),
 role: user.role,
 lastLoginAt: user.lastLoginAt,
 };
 }
}

監査ログ

interface AuditLog {
 timestamp: Date;
 userId: string;
 action: string;
 resource: string;
 resourceId: string;
 ip: string;
 userAgent: string;
 success: boolean;
}

async function auditLog(log: AuditLog) {
 await db.auditLogs.create({ data: log });
}

// ミドルウェア
function audit(action: string) {
 return async (req: Request, res: Response, next: NextFunction) => {
 const originalSend = res.send;

 res.send = function(body) {
 auditLog({
 timestamp: new Date(),
 userId: req.user?.id,
 action,
 resource: req.baseUrl,
 resourceId: req.params.id,
 ip: req.ip,
 userAgent: req.headers['user-agent'],
 success: res.statusCode < 400,
 });

 return originalSend.call(this, body);
 };

 next();
 };
}

app.delete('/users/:id', audit('DELETE_USER'), deleteUser);

関連記事

まとめ

APIセキュリティは、認証・認可、入力検証、レート制限、セキュリティヘッダーを組み合わせて実現します。OWASP API Top 10を参考に、包括的な対策を実装しましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR