APIセキュリティベストプラクティス 2026

APIセキュリティベストプラクティス 2026

REST APIとGraphQL APIのセキュリティ対策完全ガイド。認証、認可、レート制限など。

2026年3月16日8分で読了

はじめに:なぜ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セキュリティにおいて最も一般的で危険な脆弱性のリストです。

  1. BOLA(Broken Object Level Authorization) -- オブジェクトレベルの認可の欠陥
  2. Broken Authentication -- 認証メカニズムの欠陥
  3. BOPLA(Broken Object Property Level Authorization) -- プロパティレベルの認可の欠陥
  4. Unrestricted Resource Consumption -- 無制限のリソース消費
  5. BFLA(Broken Function Level Authorization) -- 機能レベルの認可の欠陥
  6. Unrestricted Access to Sensitive Business Flows -- ビジネスフローへの無制限アクセス
  7. Server Side Request Forgery -- サーバーサイドリクエストフォージェリ
  8. Security Misconfiguration -- セキュリティの設定ミス
  9. Improper Inventory Management -- 不適切なインベントリ管理
  10. 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セキュリティは単なるチェックリストではなく、継続的なプロセスです。新しい脅威や脆弱性が日々発見されるため、セキュリティ対策は常にアップデートし続ける必要があります。

本ガイドで紹介した主要なポイント:

  1. 認証の堅牢化 -- JWT、APIキーの安全な管理
  2. 認可の徹底 -- オブジェクト・機能レベルの認可チェック
  3. レート制限 -- 多層的なレート制限の実装
  4. 入力バリデーション -- Zodなどを使った厳格な検証
  5. GraphQLセキュリティ -- 深さ制限とコスト分析
  6. セキュリティヘッダー -- CSP、HSTS、CORSの適切な設定
  7. ログとモニタリング -- セキュリティイベントの継続的な監視

セキュリティ対策と合わせて、安全なパスワード生成にはパスワード生成ツールを、JWTのデバッグにはBase64エンコーダーを、APIレスポンスの確認にはJSONフォーマッターもご活用ください。Webパフォーマンスの最適化についてはWebサイト表示速度の最適化ガイドもぜひご覧ください。

関連記事