はじめに
本日、自分のSaaSプロダクトをローンチしました。フロントエンドだけでなく、苦手なバックエンドも一人で開発し、なんとか世に出すことができました。
しかし、ローンチ直後に予想外の事態が発生しました。大量のBOTによるクレジットカードのバリデーションチェック攻撃です。この記事では、その経験と対処法をまとめておきます。
攻撃の概要
何が起きたのか
ローンチ直後、決済システムに大量のトランザクション試行が発生しました。これは「カードテスティング」や「カードクラッキング」と呼ばれる攻撃で、BOTが盗まれたクレジットカード情報が有効かどうかを確認するために、実際の決済システムを使ってテストを行うものです。
攻撃の兆候
- 短時間に大量の決済試行
- 小額(1円〜100円程度)のトランザクションが多数
- 同じIPアドレスから複数のカード番号での試行
- 異常な失敗率(90%以上の決済失敗)
- 海外のIPアドレスからのアクセス集中
- ランダムな名前やメールアドレスの使用
実施した対処法
1. 緊急対応
Stripe Radarの有効化
決済プロバイダーとしてStripeを使用している場合、Stripe Radarを有効にすることで、機械学習ベースの不正検知が機能します。
// Stripe Radarは自動的に機能しますが、
// PaymentIntentの作成時に追加の情報を提供することで精度が向上
const paymentIntent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'jpy',
metadata: {
user_id: user.id,
ip_address: req.ip,
},
radar_options: {
session: sessionId, // セッション情報を提供
},
});
一時的な決済機能の停止
攻撃が激しい場合は、一時的に新規登録や決済機能を停止し、状況を確認することも選択肢の一つです。
2. Rate Limiting(レート制限)
同じIPアドレスからの連続した決済試行を制限します。
// Express + express-rate-limitの例
const rateLimit = require('express-rate-limit');
const paymentLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 5, // 15分間に最大5回まで
message: '決済試行回数が多すぎます。しばらく時間をおいてから再度お試しください。',
standardHeaders: true,
legacyHeaders: false,
});
app.post('/api/payment', paymentLimiter, async (req, res) => {
// 決済処理
});
3. CAPTCHA/reCAPTCHAの導入
決済フォームにreCAPTCHA v3を導入し、BOTとユーザーを区別します。
<!-- フロントエンド -->
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<script>
grecaptcha.ready(function() {
grecaptcha.execute('YOUR_SITE_KEY', {action: 'payment'})
.then(function(token) {
// トークンをバックエンドに送信
document.getElementById('recaptchaToken').value = token;
});
});
</script>
// バックエンド
const axios = require('axios');
async function verifyRecaptcha(token) {
const response = await axios.post(
'https://www.google.com/recaptcha/api/siteverify',
null,
{
params: {
secret: process.env.RECAPTCHA_SECRET_KEY,
response: token,
},
}
);
return response.data.success && response.data.score > 0.5;
}
app.post('/api/payment', async (req, res) => {
const isHuman = await verifyRecaptcha(req.body.recaptchaToken);
if (!isHuman) {
return res.status(403).json({ error: '不正なリクエストです' });
}
// 決済処理を続行
});
4. IPアドレスのブロックリスト
既知の悪質なIPアドレスをブロックします。
const blockedIPs = new Set([
// 攻撃元のIPアドレスをリストアップ
'192.168.1.1',
'10.0.0.1',
]);
app.use((req, res, next) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (blockedIPs.has(clientIP)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
});
5. デバイスフィンガープリント
FingerprintJSなどを使用して、デバイスを識別し、同じデバイスからの過剰な試行を防ぎます。
// フロントエンド
import FingerprintJS from '@fingerprintjs/fingerprintjs';
const fpPromise = FingerprintJS.load();
async function getFingerprint() {
const fp = await fpPromise;
const result = await fp.get();
return result.visitorId;
}
6. 3Dセキュア(3DS)の強制
高リスクなトランザクションに対して、3Dセキュアによる追加認証を要求します。
const paymentIntent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'jpy',
payment_method_options: {
card: {
request_three_d_secure: 'any', // 常に3DSを要求
},
},
});
7. メールアドレスの検証
使い捨てメールアドレスや不正なメールアドレスをブロックします。
const disposableDomains = [
'tempmail.com',
'10minutemail.com',
'guerrillamail.com',
// その他の使い捨てメールドメイン
];
function isDisposableEmail(email) {
const domain = email.split('@')[1];
return disposableDomains.includes(domain);
}
app.post('/api/register', async (req, res) => {
if (isDisposableEmail(req.body.email)) {
return res.status(400).json({
error: '使い捨てメールアドレスは使用できません'
});
}
// 登録処理を続行
});
8. 監視とアラート
異常なトラフィックを検知したら即座に通知を受け取れるようにします。
// 簡単な監視例
let failedAttempts = 0;
const THRESHOLD = 10;
const TIME_WINDOW = 5 * 60 * 1000; // 5分
setInterval(() => {
if (failedAttempts > THRESHOLD) {
sendAlert(`過去5分間に${failedAttempts}件の決済失敗が発生しました`);
}
failedAttempts = 0;
}, TIME_WINDOW);
function handlePaymentFailure() {
failedAttempts++;
// 失敗処理
}
予防策
ローンチ前に実施すべきこと
- 決済プロバイダーのセキュリティ機能を事前に有効化
- Stripe Radar
- 3Dセキュア
- リスクスコアリング
- Rate Limitingの実装
- API全体に対するレート制限
- 決済エンドポイントへの厳格な制限
- CAPTCHA/reCAPTCHAの導入
- 登録フォーム
- 決済フォーム
- 監視システムの構築
- CloudWatch、Datadog、Sentryなどの監視ツール
- 異常検知アラート
- テスト環境での負荷テスト
- 大量リクエストへの耐性確認
- セキュリティ脆弱性のチェック
ローンチ後の継続的な対策
- ログの定期的な確認
- 決済ログ
- アクセスログ
- エラーログ
- ブロックリストの更新
- 新たな攻撃元IPの追加
- 使い捨てメールドメインの更新
- セキュリティパッチの適用
- 依存ライブラリの定期的な更新
- セキュリティアップデートの即座の適用
費用面での影響
BOT攻撃による費用面での影響も考慮が必要です。
- 決済手数料: 失敗したトランザクションでも手数料が発生する場合がある
- Stripe Radarの料金: 1トランザクションあたり7円(2024年時点)
- インフラコスト: 大量リクエストによるサーバー負荷
費用を抑える工夫
// 明らかに不正なリクエストは決済プロバイダーに送信する前にブロック
app.post('/api/payment', async (req, res) => {
// 基本的なバリデーションチェック
if (!req.body.email || !req.body.cardToken) {
return res.status(400).json({ error: 'Invalid request' });
}
// reCAPTCHAチェック
const isHuman = await verifyRecaptcha(req.body.recaptchaToken);
if (!isHuman) {
return res.status(403).json({ error: 'Bot detected' });
}
// ここまで来てから初めてStripeにリクエスト
// これにより不正なリクエストによる手数料を最小化
const paymentIntent = await stripe.paymentIntents.create({...});
});
まとめ
SaaSをローンチすると、想定外の攻撃に遭遇する可能性があります。特にクレジットカード決済を扱う場合、BOTによるカードテスティング攻撃は一般的な脅威です。
重要なポイント
- 多層防御が基本: 単一の対策に頼らず、複数のセキュリティ層を組み合わせる
- 事前準備が重要: ローンチ前にセキュリティ対策を実装しておく
- 監視体制の構築: 異常を早期に検知できる仕組みを作る
- 迅速な対応: 攻撃を検知したら即座に対処する
参考リソース
一人でフルスタック開発をしていると、セキュリティ面での見落としが発生しやすいです。今回の経験を活かし、より堅牢なシステムを構築していきたいと思います。
同じような経験をされた方、または追加の対策をご存知の方がいらっしゃいましたら、ぜひコメントで共有していただけると嬉しいです。
