公開APIとセキュリティー
Photo by John Salvino on Unsplash
最近のプロジェクトでは、クライアントサイトに埋め込む <script> タグ経由でデータの取得(GET)とイベントの記録(POST)を行う仕組みを実装しました。公開APIをそのまま外部にさらすと、想定外の呼び出しや改ざんリクエストによるトラブルを招きやすいため、以下のような対策をシェアています。
共通:リクエスト元のドメインチェック
どのエンドポイントでも、まずは呼び出し元ドメインを厳密にチェックします。
- クライアント情報テーブルに
originカラムを追加 - Redis(または in-memory キャッシュ)に有効ドメイン一覧をキャッシュ
- リクエストヘッダーの
Originを参照し、キャッシュ内にない場合は 403 を返す
// express + Redis を想定したサンプル
import express from 'express';
import Redis from 'ioredis';
const app = express();
const redis = new Redis();
// キャッシュから有効ドメイン一覧を取得
async function loadAllowedOrigins() {
const origins = await redis.smembers('allowed_origins');
return new Set(origins);
}
let allowedOrigins = new Set();
loadAllowedOrigins().then(set => allowedOrigins = set);
// ミドルウェア
app.use((req, res, next) => {
const origin = req.get('Origin');
if (!origin || !allowedOrigins.has(origin)) {
return res.status(403).send('Forbidden: invalid origin');
}
next();
});GET リクエスト:レートリミットで“いじめ”を防ぐ
データ取得は状態変化を伴わないため、攻撃の主なリスクは「頻繁すぎる呼び出し」によるサーバ負荷です。
- express-rate-limit 等を使い、IP単位/ドメイン単位で秒間・分間の上限を設定
- バースト的な大量リクエストは自動拒否
import rateLimit from 'express-rate-limit';
const getLimiter = rateLimit({
windowMs: 60 * 1000, // 1分間のウィンドウ
max: 30, // 1分間に30リクエストまで
message: 'Too many requests, please try again later.'
});
app.get('/api/data', getLimiter, (req, res) => {
// 最新データを返却
res.json(fetchLatestData());
});
POST リクエスト:HMAC で改ざんとリプレイを防止
データを書き込むPOSTは、必ずリクエストの正当性(改ざんされていないか)と再送(リプレイ)を防ぐ仕組みが必要です。
- クライアント側で HMAC シグネチャを付与
- サーバー側で同じシークレットを使って検証
- シグネチャにタイムスタンプを含め、一定時間外のリクエストは拒否
// クライアント:送信前にシグネチャ計算
import crypto from 'crypto';
function signPayload(payload, secret) {
const timestamp = Date.now();
const body = JSON.stringify({ ...payload, timestamp });
const signature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return { body, signature };
}
// 例
const { body, signature } = signPayload({ event: 'conversion', userId: 123 }, 'YOUR_SECRET');
fetch('https://api.example.com/track', {
method: 'POST',
headers: { 'X-Signature': signature },
body
});
// サーバー:受信時の検証
import express from 'express';
import crypto from 'crypto';
const SECRET = process.env.HMAC_SECRET!;
const TOLERANCE_MS = 5 * 60 * 1000; // 5分
app.post('/api/track', express.json(), (req, res) => {
const signature = req.get('X-Signature')!;
const rawBody = JSON.stringify(req.body);
// 1. 改ざんチェック
const expected = crypto
.createHmac('sha256', SECRET)
.update(rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'))) {
return res.status(401).send('Invalid signature');
}
// 2. タイムスタンプチェック(リプレイ防止)
const { timestamp, ...payload } = req.body;
if (Math.abs(Date.now() - timestamp) > TOLERANCE_MS) {
return res.status(400).send('Request expired');
}
// 3. 正常処理
saveEventToDatabase(payload);
res.status(200).send('OK');
});
おわりに
- ドメインチェック → 不正な呼び出しを事前にブロック
- レートリミット → GETの濫用を抑制
- HMAC+タイムスタンプ → POSTの改ざん・リプレイを防止
これらの多層防御を組み合わせることで、公開APIでも安全性を高く保てます。ぜひ参考にしてみてください!