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

18分 read | 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);

入力検証

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インジェクション対策

// 悪い例
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 },
});

機密データの保護

// レスポンスから機密フィールドを除外
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を参考に、包括的な対策を実装しましょう。

← Back to list