APIセキュリティベストプラクティス 2026
APIセキュリティベストプラクティス 2026
REST APIとGraphQL APIのセキュリティ対策完全ガイド。認証、認可、レート制限など。
はじめに:なぜAPIセキュリティが最重要なのか
2026年の現代アプリケーションにおいて、APIはシステム間通信の根幹を担っています。モバイルアプリ、SPA(シングルページアプリケーション)、マイクロサービス、IoTデバイスなど、すべてがAPIを通じてデータを送受信しています。
しかし、APIは同時に攻撃者にとって最大の攻撃対象でもあります。OWASP(Open Worldwide Application Security Project)の調査によると、API関連のセキュリティインシデントは年々増加しており、適切なセキュリティ対策なしにAPIを公開することは、企業にとって深刻なリスクとなります。
本ガイドでは、REST APIとGraphQL APIのセキュリティベストプラクティスを、OWASP API Security Top 10 2023/2026に基づいて包括的に解説します。
OWASP API Security Top 10
まず、OWASP API Security Top 10を理解しましょう。これはAPIセキュリティにおいて最も一般的で危険な脆弱性のリストです。
- BOLA(Broken Object Level Authorization) -- オブジェクトレベルの認可の欠陥
- Broken Authentication -- 認証メカニズムの欠陥
- BOPLA(Broken Object Property Level Authorization) -- プロパティレベルの認可の欠陥
- Unrestricted Resource Consumption -- 無制限のリソース消費
- BFLA(Broken Function Level Authorization) -- 機能レベルの認可の欠陥
- Unrestricted Access to Sensitive Business Flows -- ビジネスフローへの無制限アクセス
- Server Side Request Forgery -- サーバーサイドリクエストフォージェリ
- Security Misconfiguration -- セキュリティの設定ミス
- Improper Inventory Management -- 不適切なインベントリ管理
- Unsafe Consumption of APIs -- APIの安全でない利用
1. 認証(Authentication)のベストプラクティス
JWT(JSON Web Token)の安全な実装
import jwt from "jsonwebtoken";
import { randomBytes } from "crypto";
// JWTの安全な生成
function generateTokens(userId: string, roles: string[]) {
const accessToken = jwt.sign(
{
sub: userId,
roles,
type: "access",
jti: randomBytes(16).toString("hex"), // ユニークID(リプレイ攻撃対策)
},
process.env.JWT_SECRET!,
{
algorithm: "RS256", // 非対称暗号を推奨(HS256よりも安全)
expiresIn: "15m", // 短い有効期限(15分以下を推奨)
issuer: "api.example.com",
audience: "app.example.com",
}
);
const refreshToken = jwt.sign(
{
sub: userId,
type: "refresh",
jti: randomBytes(16).toString("hex"),
},
process.env.JWT_REFRESH_SECRET!,
{
algorithm: "RS256",
expiresIn: "7d", // リフレッシュトークンは長め
issuer: "api.example.com",
}
);
return { accessToken, refreshToken };
}
// JWTの検証
function verifyAccessToken(token: string) {
try {
const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY!, {
algorithms: ["RS256"], // 使用するアルゴリズムを明示的に指定
issuer: "api.example.com",
audience: "app.example.com",
});
return { valid: true, payload };
} catch (error) {
return { valid: false, error: (error as Error).message };
}
}
トークンローテーションの実装
// リフレッシュトークンのローテーション
async function rotateRefreshToken(oldRefreshToken: string) {
// 1. トークンの検証
const { valid, payload } = verifyRefreshToken(oldRefreshToken);
if (!valid) {
throw new AuthError("無効なリフレッシュトークン");
}
// 2. トークンが使用済みか確認(リプレイ攻撃検出)
const isUsed = await tokenStore.isUsed(payload.jti);
if (isUsed) {
// トークンの再利用を検出 → すべてのトークンを無効化
await tokenStore.revokeAllForUser(payload.sub);
throw new AuthError("トークンの不正な再利用を検出しました");
}
// 3. 古いトークンを使用済みとしてマーク
await tokenStore.markAsUsed(payload.jti);
// 4. 新しいトークンペアを生成
const user = await db.user.findUnique({ where: { id: payload.sub } });
return generateTokens(user.id, user.roles);
}
APIキーの管理
import { createHash, randomBytes, timingSafeEqual } from "crypto";
// APIキーの安全な生成
function generateApiKey(): { key: string; hash: string; prefix: string } {
const key = `sk_live_${randomBytes(32).toString("hex")}`;
const prefix = key.substring(0, 12); // 表示用プレフィックス
const hash = createHash("sha256").update(key).digest("hex");
return { key, hash, prefix };
}
// APIキーの検証(タイミング攻撃に耐性あり)
async function validateApiKey(providedKey: string): Promise<boolean> {
const providedHash = createHash("sha256").update(providedKey).digest("hex");
const storedRecord = await db.apiKey.findFirst({
where: { prefix: providedKey.substring(0, 12) },
});
if (!storedRecord) return false;
return timingSafeEqual(
Buffer.from(providedHash),
Buffer.from(storedRecord.hash)
);
}
2. 認可(Authorization)のベストプラクティス
BOLA(オブジェクトレベル認可)対策
// 悪い例:認可チェックなし
app.get("/api/users/:id/orders", async (req, res) => {
const orders = await db.order.findMany({
where: { userId: req.params.id }, // 他人の注文も取得できてしまう!
});
res.json(orders);
});
// 良い例:オブジェクトレベルの認可チェック
app.get("/api/users/:id/orders", authenticate, async (req, res) => {
// 認証済みユーザーが対象リソースの所有者か確認
if (req.user.id !== req.params.id && !req.user.roles.includes("admin")) {
return res.status(403).json({
error: "このリソースにアクセスする権限がありません",
});
}
const orders = await db.order.findMany({
where: { userId: req.params.id },
});
res.json(orders);
});
RBAC(ロールベースアクセス制御)の実装
// パーミッション定義
const PERMISSIONS = {
"user:read": ["admin", "moderator", "user"],
"user:write": ["admin"],
"user:delete": ["admin"],
"post:read": ["admin", "moderator", "user"],
"post:write": ["admin", "moderator", "user"],
"post:delete": ["admin", "moderator"],
"settings:read": ["admin"],
"settings:write": ["admin"],
} as const;
type Permission = keyof typeof PERMISSIONS;
// パーミッションチェックミドルウェア
function requirePermission(...permissions: Permission[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userRoles = req.user?.roles || [];
const hasPermission = permissions.every((permission) => {
const allowedRoles = PERMISSIONS[permission];
return userRoles.some((role: string) =>
(allowedRoles as readonly string[]).includes(role)
);
});
if (!hasPermission) {
return res.status(403).json({
error: "権限が不足しています",
required: permissions,
});
}
next();
};
}
// 使用例
app.delete(
"/api/users/:id",
authenticate,
requirePermission("user:delete"),
async (req, res) => {
await db.user.delete({ where: { id: req.params.id } });
res.status(204).send();
}
);
3. レート制限(Rate Limiting)
多層レート制限の実装
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
// グローバルレート制限
const globalLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 15 * 60 * 1000, // 15分
max: 1000, // IPあたり1000リクエスト
standardHeaders: true, // RateLimit-* ヘッダーを返す
legacyHeaders: false,
message: {
error: "リクエスト数が制限を超えました。しばらくしてからお試しください。",
retryAfter: "15分後に再試行可能です",
},
});
// 認証エンドポイント用の厳格なレート制限
const authLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 15 * 60 * 1000,
max: 10, // 15分あたり10回まで
skipSuccessfulRequests: true, // 成功したリクエストはカウントしない
message: {
error: "ログイン試行回数が上限に達しました",
},
});
// APIキー別のレート制限
const apiKeyLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 60 * 1000, // 1分
max: 100,
keyGenerator: (req) => req.headers["x-api-key"] as string || req.ip,
});
// 適用
app.use("/api/", globalLimiter);
app.use("/api/auth/", authLimiter);
app.use("/api/v1/", apiKeyLimiter);
スライディングウィンドウの実装
// Redisを使用したスライディングウィンドウレート制限
async function slidingWindowRateLimit(
key: string,
limit: number,
windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
const now = Date.now();
const windowStart = now - windowMs;
const pipeline = redis.pipeline();
// 古いエントリを削除
pipeline.zremrangebyscore(key, 0, windowStart);
// 現在のリクエストを追加
pipeline.zadd(key, now, `${now}-${Math.random()}`);
// 現在のウィンドウ内のリクエスト数を取得
pipeline.zcard(key);
// TTLを設定
pipeline.pexpire(key, windowMs);
const results = await pipeline.exec();
const count = results![2][1] as number;
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
resetAt: new Date(now + windowMs),
};
}
4. 入力バリデーション
Zodを使用した厳格なバリデーション
import { z } from "zod";
// ユーザー作成のバリデーションスキーマ
const createUserSchema = z.object({
name: z
.string()
.min(1, "名前は必須です")
.max(100, "名前は100文字以内で入力してください")
.regex(
/^[a-zA-Z\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\u0020]+$/,
"使用できない文字が含まれています"
),
email: z.string().email("有効なメールアドレスを入力してください"),
password: z
.string()
.min(12, "パスワードは12文字以上で入力してください")
.regex(/[A-Z]/, "大文字を1文字以上含めてください")
.regex(/[a-z]/, "小文字を1文字以上含めてください")
.regex(/[0-9]/, "数字を1文字以上含めてください")
.regex(/[^A-Za-z0-9]/, "特殊文字を1文字以上含めてください"),
age: z
.number()
.int()
.min(13, "13歳以上である必要があります")
.max(150)
.optional(),
role: z.enum(["user", "moderator"]).default("user"),
});
// バリデーションミドルウェア
function validate<T extends z.ZodSchema>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: "バリデーションエラー",
details: result.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
})),
});
}
req.body = result.data; // バリデーション済みデータで置換
next();
};
}
app.post("/api/users", validate(createUserSchema), async (req, res) => {
// req.body は型安全かつバリデーション済み
const user = await createUser(req.body);
res.status(201).json(user);
});
SQLインジェクション対策
// 悪い例:SQLインジェクションの脆弱性
const query = `SELECT * FROM users WHERE id = '${req.params.id}'`;
// 良い例:パラメータ化されたクエリ
// Prisma ORM使用
const user = await prisma.user.findUnique({
where: { id: req.params.id },
});
// Knex.js使用
const user = await knex("users").where("id", req.params.id).first();
// 生のSQLが必要な場合はパラメータバインディングを使用
const [rows] = await pool.execute(
"SELECT * FROM users WHERE id = ? AND status = ?",
[req.params.id, "active"]
);
5. GraphQLセキュリティ
GraphQL特有のセキュリティ課題とその対策を解説します。
クエリの深さ制限とコスト分析
import depthLimit from "graphql-depth-limit";
import { createComplexityLimitRule } from "graphql-validation-complexity";
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// クエリの深さを制限(ネスト攻撃対策)
depthLimit(10),
// クエリの複雑さを制限
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
}),
],
plugins: [
// クエリのタイムアウト
{
requestDidStart() {
const timeout = setTimeout(() => {
throw new Error("クエリのタイムアウト(30秒)");
}, 30000);
return {
willSendResponse() {
clearTimeout(timeout);
},
};
},
},
],
});
フィールドレベルの認可
// GraphQL Shield を使用したフィールドレベル認可
import { shield, rule, allow, deny } from "graphql-shield";
const isAuthenticated = rule({ cache: "contextual" })(
async (parent, args, ctx) => {
return ctx.user !== null;
}
);
const isAdmin = rule({ cache: "contextual" })(async (parent, args, ctx) => {
return ctx.user?.role === "admin";
});
const isOwner = rule({ cache: "strict" })(async (parent, args, ctx) => {
const resource = await db.findResource(args.id);
return resource?.ownerId === ctx.user?.id;
});
const permissions = shield(
{
Query: {
users: isAdmin,
user: isAuthenticated,
publicPosts: allow,
},
Mutation: {
createUser: isAdmin,
updateUser: isOwner,
deleteUser: isAdmin,
},
User: {
email: isOwner, // メールアドレスは本人のみ閲覧可能
role: isAdmin,
},
},
{ fallbackRule: deny }
);
6. セキュリティヘッダーの設定
import helmet from "helmet";
// Helmetを使用したセキュリティヘッダーの設定
app.use(
helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'strict-dynamic'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
// HSTS(HTTP Strict Transport Security)
hsts: {
maxAge: 31536000, // 1年
includeSubDomains: true,
preload: true,
},
// X-Content-Type-Optionsの設定
noSniff: true,
// X-Frame-Optionsの設定
frameguard: { action: "deny" },
// Referrer Policyの設定
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
})
);
// CORS設定
import cors from "cors";
app.use(
cors({
origin: ["https://app.example.com", "https://admin.example.com"],
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
allowedHeaders: ["Content-Type", "Authorization", "X-API-Key"],
credentials: true,
maxAge: 86400, // プリフライトリクエストのキャッシュ(24時間)
})
);
7. ログとモニタリング
セキュリティイベントのログ
import winston from "winston";
const securityLogger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: "api-security" },
transports: [
new winston.transports.File({ filename: "security.log" }),
new winston.transports.Console(),
],
});
// セキュリティイベントのログ記録
function logSecurityEvent(event: {
type: string;
severity: "low" | "medium" | "high" | "critical";
userId?: string;
ip: string;
details: Record<string, any>;
}) {
securityLogger.warn("Security Event", {
...event,
timestamp: new Date().toISOString(),
});
// 重大なイベントの場合はアラートを送信
if (event.severity === "critical") {
sendAlert({
channel: "security",
message: `重大なセキュリティイベント: ${event.type}`,
details: event,
});
}
}
// 使用例
app.use((req, res, next) => {
// レスポンス送信後にログを記録
res.on("finish", () => {
if (res.statusCode === 401) {
logSecurityEvent({
type: "AUTHENTICATION_FAILURE",
severity: "medium",
ip: req.ip,
details: {
path: req.path,
method: req.method,
userAgent: req.headers["user-agent"],
},
});
}
if (res.statusCode === 403) {
logSecurityEvent({
type: "AUTHORIZATION_FAILURE",
severity: "high",
userId: req.user?.id,
ip: req.ip,
details: {
path: req.path,
method: req.method,
attemptedAction: req.body?.action,
},
});
}
});
next();
});
8. APIバージョニングとセキュリティ
// URLベースのバージョニング
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);
// 古いバージョンの非推奨通知
app.use("/api/v1", (req, res, next) => {
res.set({
Deprecation: "true",
Sunset: "2026-12-31",
Link: '<https://api.example.com/api/v2>; rel="successor-version"',
});
next();
});
セキュリティチェックリスト
APIをリリースする前に、以下のチェックリストを確認しましょう:
認証・認可
- すべてのエンドポイントに適切な認証を実装している
- オブジェクトレベルの認可チェックを行っている
- JWTの有効期限を短く設定している(15分以下)
- リフレッシュトークンのローテーションを実装している
- APIキーは安全にハッシュ化して保存している
入力バリデーション
- すべての入力パラメータをバリデーションしている
- SQLインジェクション対策(パラメータ化クエリ)を行っている
- XSS対策(出力のエスケープ)を行っている
- ファイルアップロードの検証を行っている
レート制限
- グローバルレート制限を設定している
- 認証エンドポイントに厳格なレート制限を設定している
- ユーザー/APIキー別のレート制限を設定している
通信セキュリティ
- HTTPS(TLS 1.3)を強制している
- 適切なCORS設定を行っている
- セキュリティヘッダーを設定している
モニタリング
- セキュリティイベントのログを記録している
- 異常なアクセスパターンのアラートを設定している
- 定期的なセキュリティ監査を実施している
まとめ
APIセキュリティは単なるチェックリストではなく、継続的なプロセスです。新しい脅威や脆弱性が日々発見されるため、セキュリティ対策は常にアップデートし続ける必要があります。
本ガイドで紹介した主要なポイント:
- 認証の堅牢化 -- JWT、APIキーの安全な管理
- 認可の徹底 -- オブジェクト・機能レベルの認可チェック
- レート制限 -- 多層的なレート制限の実装
- 入力バリデーション -- Zodなどを使った厳格な検証
- GraphQLセキュリティ -- 深さ制限とコスト分析
- セキュリティヘッダー -- CSP、HSTS、CORSの適切な設定
- ログとモニタリング -- セキュリティイベントの継続的な監視
セキュリティ対策と合わせて、安全なパスワード生成にはパスワード生成ツールを、JWTのデバッグにはBase64エンコーダーを、APIレスポンスの確認にはJSONフォーマッターもご活用ください。Webパフォーマンスの最適化についてはWebサイト表示速度の最適化ガイドもぜひご覧ください。