OWASP Top 10を理解する|Webアプリの主要脆弱性と対策を解説【2026年版】

kento_morota 22分で読めます

Webアプリケーションのセキュリティ対策は、開発者にとって必須のスキルです。しかし、「何から対策すればいいかわからない」という声も少なくありません。そこで指針となるのが、OWASP(Open Worldwide Application Security Project)が公開するOWASP Top 10です。

この記事では、OWASP Top 10に掲載されている主要な脆弱性について、攻撃の仕組みと具体的な対策コードを解説します。

OWASP Top 10とは?

OWASPは、Webアプリケーションのセキュリティ向上を目指す国際的な非営利組織です。OWASP Top 10は、Webアプリケーションで最も深刻かつ頻繁に見られるセキュリティリスクのランキングで、数年ごとに更新されています。

OWASP Top 10 2021のカテゴリ

  • A01:アクセス制御の不備(Broken Access Control)
  • A02:暗号化の失敗(Cryptographic Failures)
  • A03:インジェクション(Injection)
  • A04:安全でない設計(Insecure Design)
  • A05:セキュリティの設定ミス(Security Misconfiguration)
  • A06:脆弱で古いコンポーネント(Vulnerable and Outdated Components)
  • A07:認証と識別の失敗(Identification and Authentication Failures)
  • A08:ソフトウェアとデータの整合性の不備(Software and Data Integrity Failures)
  • A09:セキュリティログと監視の不備(Security Logging and Monitoring Failures)
  • A10:サーバーサイドリクエストフォージェリ(SSRF)

以降では、特に実装レベルで対策が必要な主要カテゴリについて、具体的なコード例とともに解説します。

A01:アクセス制御の不備

アクセス制御の不備は、OWASP Top 10で1位にランクされている最も深刻なリスクです。認可されていないユーザーが、本来アクセスできないデータや機能にアクセスできてしまう問題です。

攻撃の例

// 脆弱なAPI: URLのパラメータを変えるだけで他人のデータにアクセスできる
// GET /api/users/123/profile → 自分のプロフィール
// GET /api/users/456/profile → 他人のプロフィール(本来はアクセス不可)

// 脆弱な実装
app.get('/api/users/:id/profile', async (req, res) => {
  const user = await db.user.findById(req.params.id);
  res.json(user);  // 誰のデータでも返してしまう
});

対策コード

// 対策1: 認可チェックを実装
app.get('/api/users/:id/profile', authenticate, async (req, res) => {
  // ログインユーザーのIDと要求されたIDを比較
  if (req.user.id !== req.params.id && req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Access denied' });
  }
  const user = await db.user.findById(req.params.id);
  res.json(user);
});

// 対策2: セッションのユーザーIDを使用(IDOR対策)
app.get('/api/me/profile', authenticate, async (req, res) => {
  // URLにIDを含めず、セッションから取得
  const user = await db.user.findById(req.user.id);
  res.json(user);
});
// 対策3: ミドルウェアでロールベースのアクセス制御
function authorize(...roles: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

// 管理者のみアクセス可能
app.delete('/api/users/:id', authenticate, authorize('admin'), async (req, res) => {
  await db.user.delete(req.params.id);
  res.status(204).send();
});

A03:インジェクション

インジェクション攻撃は、ユーザーの入力データがコマンドやクエリの一部として解釈されることで発生します。SQLインジェクション、NoSQLインジェクション、OSコマンドインジェクションなどが含まれます。

SQLインジェクションの例と対策

// 脆弱な実装: 文字列結合でSQLを構築
app.get('/api/users', async (req, res) => {
  const query = `SELECT * FROM users WHERE name = '${req.query.name}'`;
  const users = await db.query(query);
  // 攻撃例: ?name=' OR '1'='1
  // → SELECT * FROM users WHERE name = '' OR '1'='1' (全ユーザーが取得される)
  res.json(users);
});

// 対策: パラメータ化クエリ(プリペアドステートメント)
app.get('/api/users', async (req, res) => {
  const query = 'SELECT * FROM users WHERE name = $1';
  const users = await db.query(query, [req.query.name]);
  res.json(users);
});
// ORMを使った安全な実装(Prisma)
const users = await prisma.user.findMany({
  where: {
    name: req.query.name as string,  // Prismaが自動的にエスケープ
  },
});

XSS(クロスサイトスクリプティング)の対策

// 脆弱な実装: ユーザー入力をそのままHTMLに挿入
element.innerHTML = userComment;
// 攻撃例: <script>document.cookie</script>

// 対策1: textContentを使う
element.textContent = userComment;

// 対策2: サーバーサイドでエスケープ
import DOMPurify from 'dompurify';
const sanitized = DOMPurify.sanitize(userComment);

// 対策3: Content Security Policy(CSP)ヘッダーを設定
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
  );
  next();
});

入力バリデーションの実装

// zodによるバリデーション
import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1).max(100).regex(/^[\p{L}\p{N}\s]+$/u),
  email: z.string().email().max(255),
  age: z.number().int().min(0).max(150),
});

app.post('/api/users', async (req, res) => {
  const result = userSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.errors });
  }
  // バリデーション済みのデータを使用
  const user = await db.user.create(result.data);
  res.status(201).json(user);
});

A02:暗号化の失敗

機密データの保護に関する問題です。パスワードの平文保存、弱い暗号アルゴリズムの使用、HTTPS未対応などが該当します。

パスワードの安全な保存

// 悪い例: 平文で保存
await db.user.create({ password: req.body.password });

// 悪い例: MD5やSHA-1でハッシュ化(脆弱)
const hash = crypto.createHash('md5').update(password).digest('hex');

// 良い例: bcryptでハッシュ化
import bcrypt from 'bcrypt';

// パスワードのハッシュ化(登録時)
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(req.body.password, saltRounds);
await db.user.create({ password: hashedPassword });

// パスワードの検証(ログイン時)
const isValid = await bcrypt.compare(req.body.password, user.password);
if (!isValid) {
  return res.status(401).json({ error: 'Invalid credentials' });
}

HTTPS/TLSの強制

// HTTPをHTTPSにリダイレクト
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    return res.redirect(301, `https://${req.header('host')}${req.url}`);
  }
  next();
});

// HSTSヘッダーの設定
app.use((req, res, next) => {
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );
  next();
});

A05:セキュリティの設定ミス

デフォルト設定のまま本番運用する、不要な機能を有効にしたままにする、エラーメッセージで内部情報を公開するなど、設定に起因するセキュリティリスクです。

セキュリティヘッダーの設定

// Helmetミドルウェアで主要なセキュリティヘッダーを一括設定
import helmet from 'helmet';

app.use(helmet());

// 個別に設定する場合
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "https://api.example.com"],
    frameSrc: ["'none'"],
    objectSrc: ["'none'"],
  },
}));

app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));
app.use(helmet.frameguard({ action: 'deny' }));

エラーハンドリングの適切な実装

// 悪い例: 内部情報を公開
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack,         // スタックトレースの公開は危険
    query: req.query,         // リクエスト情報の公開
    dbConnection: db.config,  // DB接続情報の公開
  });
});

// 良い例: 一般的なメッセージのみ返す
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // ログには詳細を記録
  console.error('Internal Error:', {
    message: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method,
  });

  // クライアントには一般的なメッセージのみ
  res.status(500).json({
    error: 'Internal Server Error',
    requestId: req.id,  // 問い合わせ用のIDのみ
  });
});

A07:認証と識別の失敗

認証メカニズムの不備により、攻撃者がユーザーのアカウントを乗っ取ったり、認証をバイパスしたりできる問題です。

JWTの安全な実装

import jwt from 'jsonwebtoken';

// JWTの生成
function generateTokens(userId: string) {
  // アクセストークン(短い有効期限)
  const accessToken = jwt.sign(
    { sub: userId, type: 'access' },
    process.env.JWT_SECRET!,
    { expiresIn: '15m', algorithm: 'HS256' }
  );

  // リフレッシュトークン(長い有効期限)
  const refreshToken = jwt.sign(
    { sub: userId, type: 'refresh' },
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: '7d', algorithm: 'HS256' }
  );

  return { accessToken, refreshToken };
}

// JWTの検証ミドルウェア
function authenticate(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }

  const token = authHeader.split(' ')[1];
  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' });
  }
}

ブルートフォース攻撃の対策

import rateLimit from 'express-rate-limit';

// ログインエンドポイントにレート制限を適用
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15分
  max: 5,                     // 最大5回
  message: { error: 'Too many login attempts. Please try again later.' },
  standardHeaders: true,
  legacyHeaders: false,
  // IPとユーザー名の組み合わせでレート制限
  keyGenerator: (req) => {
    return `${req.ip}-${req.body.email}`;
  },
});

app.post('/api/auth/login', loginLimiter, async (req, res) => {
  // ログイン処理
});

A10:サーバーサイドリクエストフォージェリ(SSRF)

SSRFは、サーバーが外部リソースを取得する際に、攻撃者がリクエスト先を操作してサーバー内部のリソースにアクセスする攻撃です。

SSRF攻撃の例と対策

// 脆弱な実装: ユーザーが指定したURLからデータを取得
app.post('/api/fetch-url', async (req, res) => {
  const response = await fetch(req.body.url);
  const data = await response.text();
  res.json({ content: data });
  // 攻撃例: url = "http://169.254.169.254/latest/meta-data/"
  // → AWS EC2のメタデータにアクセスされる
});

// 対策: URLのバリデーションとホワイトリスト
import { URL } from 'url';

const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com'];

app.post('/api/fetch-url', async (req, res) => {
  let parsedUrl: URL;
  try {
    parsedUrl = new URL(req.body.url);
  } catch {
    return res.status(400).json({ error: 'Invalid URL' });
  }

  // プロトコルのチェック
  if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
    return res.status(400).json({ error: 'Invalid protocol' });
  }

  // ホワイトリストの確認
  if (!ALLOWED_HOSTS.includes(parsedUrl.hostname)) {
    return res.status(400).json({ error: 'Host not allowed' });
  }

  // プライベートIPのブロック
  const isPrivateIP = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|169\.254\.)/.test(
    parsedUrl.hostname
  );
  if (isPrivateIP) {
    return res.status(400).json({ error: 'Private IP not allowed' });
  }

  const response = await fetch(parsedUrl.toString());
  const data = await response.text();
  res.json({ content: data });
});

セキュリティテストの自動化

脆弱性対策が正しく実装されているかを継続的に検証するため、セキュリティテストを自動化しましょう。

依存関係の脆弱性チェック

# npm auditによるチェック
npm audit

# 自動修正(メジャーバージョンアップは除く)
npm audit fix

# GitHub Dependabotの設定
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

セキュリティテストの例

// セキュリティ関連のテスト例(Jest)
describe('Security Tests', () => {
  describe('Authentication', () => {
    it('認証なしのリクエストを拒否する', async () => {
      const res = await request(app).get('/api/users/me');
      expect(res.status).toBe(401);
    });

    it('無効なトークンを拒否する', async () => {
      const res = await request(app)
        .get('/api/users/me')
        .set('Authorization', 'Bearer invalid-token');
      expect(res.status).toBe(401);
    });
  });

  describe('Access Control', () => {
    it('他ユーザーのデータへのアクセスを拒否する', async () => {
      const token = generateToken(user1.id);
      const res = await request(app)
        .get(`/api/users/${user2.id}/profile`)
        .set('Authorization', `Bearer ${token}`);
      expect(res.status).toBe(403);
    });
  });

  describe('Input Validation', () => {
    it('SQLインジェクションの試行を防ぐ', async () => {
      const res = await request(app)
        .get("/api/users?name=' OR '1'='1");
      expect(res.status).toBe(400);
    });

    it('XSSペイロードをサニタイズする', async () => {
      const res = await request(app)
        .post('/api/comments')
        .send({ content: '' });
      expect(res.body.content).not.toContain('  

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう