REST APIセキュリティ:基本的なミスでシステムを「崩壊」させないために

Security tutorial - IT technology blog
Security tutorial - IT technology blog

あなたのAPIはハッカーに対して「無防備」になっていませんか?

ウェブ開発を始めたばかりの多くの人は、APIが正常に動作し、正しいJSONを返すことだけを優先しがちです。しかし、セキュリティ対策のないAPIをインターネットに公開することは、ハッカーに「どうぞ攻撃してください」と言っているようなものです。実際、筆者もかつてテスト用エンドポイントの不備からサーバーへのSSHブルートフォース攻撃を受け、徹夜で対応した経験があります。その教訓から学んだのは、プロジェクトの規模に関わらず, 最初の1行目のコードからセキュリティを意識すべきだということです。

APIは単なるデータのやり取りの場所ではありません。外部の世界があなたのデータベースに触れることができる唯一のゲートウェイです. Akamaiのレポートによると、現在のウェブ攻撃の75%がAPIを標的にしています。防御層がなければ、ユーザーデータは盗まれ、システム全体が一瞬で消去されてしまう可能性さえあります。

「不変」の3つの防御層

APIを包括的に保護するために、シニアエンジニアが必ず求める3つのルールを常に適用しています tour:

  • Authentication(認証): 本人確認。システムに入る前に、自分が誰であるかを証明する必要があります。
  • Authorization(認可): 権限管理。例えば、一般顧客がショップオーナーの商品を削除できてはいけません。
  • Data Validation(データバリデーション): 不正コードのフィルタリング。SQLインジェクションやXSSなどの手法を、入り口(バリデーション段階)で阻止します。

1. JSON Web Token (JWT) によるスマートな認証

従来のセッション管理は、サーバーのスケール(拡張)時に課題が生じることがあります。JWTはステートレス(サーバー側に状態を保存しない)な仕組みでこれを解決します。ログイン成功時、サーバーはトークンを返します。それ以降のリクエストでは、クライアントはヘッダーにこのトークンを含めるだけで済みます。

Node.jsでJWTをチェックするミドルウェアを安全に実装する方法は以下の通りです:

const jwt = require('jsonwebtoken');

const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    // トークンがない場合はエラーを返す
    if (!token) return res.status(401).json({ message: "この操作を行うにはログインが必要です!" });

    jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
        // トークンが無効または期限切れの場合
        if (err) return res.status(403).json({ message: "トークンの期限が切れているか、無効です!" });
        req.user = user;
        next();
    });
};

ヒント: トークンの有効期限(exp)は15〜30分程度と短く設定し、セキュリティを高めるためにRefresh Tokenを併用しましょう。

2. 認可 (Authorization) – IDORの脆弱性を阻止する

認証が完了しただけでは不十分です。非常によくあるミスがIDOR(不適切な直接オブジェクト参照)です。これは、ユーザーAがURLのidを変更するだけで、ユーザーBの注文内容を閲覧できてしまうような脆弱性です。

最善の解決策はRBAC(ロールベースアクセス制御)を使用することです。コントローラーへのアクセスを許可する前に、ユーザーの役割を確認するミドルウェアが必要です。

const authorizeRoles = (...allowedRoles) => {
    return (req, res, next) => {
        // ユーザーのロールが許可されたリストに含まれているか確認
        if (!allowedRoles.includes(req.user.role)) {
            return res.status(403).json({ message: "このエリアへのアクセス権限がありません!" });
        }
        next();
    };
};

// 管理者(Admin)のみが商品を削除可能
router.delete('/product/:id', authenticateToken, authorizeRoles('admin'), deleteProduct);

3. データバリデーション – クライアントを絶対に信用しない

クライアントから送られてくるデータは、常に「汚染されている」と想定してください。データ型のチェックを怠ると、ハッカーは数値の代わりに破壊的なスクリプトを送信してくる可能性があります。

手動でif/elseを書く代わりに、JoiZodなどのライブラリを使用して厳密なスキーマを定義しましょう:

const Joi = require('joi');

const schema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required(),
    email: Joi.string().email().required(),
});

// 入力データのバリデーション実行
const { error } = schema.validate(req.body);
if (error) return res.status(400).send(error.details[0].message);

その他の実践的な対策

上記の3つの柱に加えて、自動攻撃に対してAPIをより強固にするために、以下の防御層を追加することをお勧めします:

HTTPSの使用を必須にする

HTTPSの使用を必須にすることがなければ、その後のセキュリティ対策はほぼ無意味です。ハッカーは中間者攻撃(Man-in-the-middle)を使用して、通信を「盗聴」し、簡単にトークンを奪うことができます。TLS 1.2または1.3を使用して、すべてのデータ転送を暗号化してください。

Rate Limiting – ボットとスパムを阻止する

1つのIPから毎秒数百のリクエストが送信される場合、それは間違いなくシステムを破壊しようとする、あるいはパスワードをブルートフォース攻撃しようとするボットです。express-rate-limitライブラリは、一定時間内のリクエスト数を制限するのに役立ちます。

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
    windowMs: 1 * 60 * 1000, // 1分間
    max: 60, // 1つのIPから1分間に最大60リクエストまで
    message: "リクエストが速すぎます。しばらくお待ちください!"
});

app.use('/api/', limiter);

Helmetを使用して技術情報を隠蔽する

デフォルトでは、ExpressはX-Powered-By: Expressというヘッダーを付与します。これは、ハッカーに使用しているフレームワークのバージョンの脆弱性を探すヒントを与えてしまいます。helmetをインストールするだけで、このライブラリが機密性の高いヘッダーを自動的に削除し、基本的なセキュリティ設定を行ってくれます。

おわりに

APIセキュリティはゴールではなく、継続的なプロセスです。深夜にサーバーが攻撃されてから慌てて修正するのではなく、安全なコードを書く習慣を身につけ、定期的にログを確認し、ライブラリのセキュリティパッチを迅速に適用しましょう。堅牢なシステムを構築できることを願っています!

Share: