みなさんこんにちは! 生成AIを使ったアプリ開発、最近すごく盛んですよね。「こんな機能を作ってみたよ!」と動かしてみる瞬間は、エンジニアとして本当にワクワクするものです。
でも、ちょっと待ってくださいね。
「ユーザーが入力するテキストは、どれだけ長くても受け付けちゃえばいいや!」なんて、無防備にAPIをつないでいませんか?
実はそこ、サイバー攻撃者からすると「一番いじりがいのある、お店のど真ん中の金庫の鍵」みたいなものなんです。
今回は、最近じわじわと被害が増えている「LLM(大規模言語モデル)のコンテキストウィンドウに対するDoS攻撃」について、身近な例えを交えながら、どうやって守っていけばいいのかを一緒に優しく学んでいきましょう!
—
1. 家の鍵で例える「LLMのコンテキストウィンドウ攻撃」
突然ですが、あなたの家に「どんな手紙でも、何千ページある辞書でも、一言一句すべて読み込んで完璧に返事をしてくれる超天才の執事」が住んでいると想像してください。すごい便利ですよね。
この執事には一つのルールがあります。それは、「一度に覚えられる記憶の容量(コンテキストウィンドウ)には限界があるけれど、目の前に置かれたものは全部、脳みそをフル回転させて一気に読み込もうとする」という性質です。
ここに、悪意を持った泥棒(攻撃者)がやってきました。
泥棒は、こう考えました。
「この執事に、わざと文字がビッシリ書かれた『10万ページの超分厚い意味不明な小説』を読ませ続けたらどうなるだろう?」
執事は真面目ですから、その分厚い本を全部頭に叩き込もうと必死になります。脳みそが沸騰し、計算機(サーバー)のメモリはパンクし、ファンは猛回転。結果として、執事は疲れ果ててしまい、本当に助けを求めている家族(一般のユーザー)の呼びかけに一切応じられなくなってしまいました。
これが、LLMにおける「コンテキストウィンドウに対するDoS(サービス妨害)攻撃」の正体です。AIの「全部読んで理解しようとする性質」を逆手に取った、リソース食い潰し作戦なんですね。
—
2. なぜ、この攻撃が狙われるのか?
通常のWebサイトに対するDoS攻撃(例えば、大量のアクセスを送りつけるDDoS攻撃など)は、ネットワークの防衛システムが進化しているため、最近ではファイアウォールなどで比較的弾きやすくなっています。
しかし、生成AIのAPIに対する攻撃は、泥棒が「正規の正面玄関から、めちゃくちゃ重い荷物を何個も抱えて入り込んでくる」ようなものなのです。
サーバー側からすると、「これ、本当にユーザーが使いたい正当なリクエストかな? それとも嫌がらせの巨大データかな?」の判断がつきにくいため、丸ごと処理しようとして一撃でダウンしてしまうのです。
「じゃあ、どうやってこの重い荷物を防げばいいの?」と思いますよね。
安心してください。対策は大きく分けて「① 入口での荷物のサイズ制限(入力トークン数制限)」と「② 回数制限(レートリミット)」の2つをしっかり行うだけです。一歩ずつ見ていきましょう!
—
3. 実装で学ぶ! 2つの鉄壁の防衛策
ここからは、実際のWebアプリケーション開発(Node.js / Express環境をイメージしてください)を例に、具体的なコードを見ていきましょう。難しく見えますが、日本語のコメントを入れながら丁寧に解説するので安心してくださいね。
防衛策①:入力トークン数を制限する(荷物の大きさを測る)
AIにテキストを渡す前に、「おいおい、この入力、長すぎないか?」と文字数やトークン数(AIが文字を数える単位)をカウントして、規定値を超えていたら「大きすぎます!」と門前払いする仕組みを作ります。
const express = require('express');
const app = express();
// JSONボディをパースする(サイズ上限をあらかじめ厳しめに設定しておきます)
app.use(express.json({ limit: '10kb' }));
// 簡易的なトークン数の目安を計算する関数
// ※実務では OpenAI公式の `tiktoken` などのライブラリを使うのが確実です
function estimateTokenCount(text) {
// ここではざっくり「文字数 / 4」をトークン数の目安とします
return Math.ceil(text.length / 4);
}
app.post('/api/chat', (req, res) => {
const userPrompt = req.body.prompt;
if (!userPrompt) {
return res.status(400).json({ error: 'プロンプトが空です。' });
}
// 1. 入力されたプロンプトのトークン数を計算
const tokenCount = estimateTokenCount(userPrompt);
// 2. 許容する最大トークン数(例: 最大 1,000 トークンまで)
const MAX_ALLOWED_TOKENS = 1000;
if (tokenCount > MAX_ALLOWED_TOKENS) {
// 巨大なプロンプトが来たら、AIを呼び出さずにここで即座に弾く!
console.warn(`[セキュリティ警告] 制限を超える巨大なプロンプトを検知しました: ${tokenCount} tokens`);
return res.status(413).json({
error: '入力が長すぎます。もう少し短い文章で質問してください。'
});
}
// --- この下で安全にAI(LLM)のAPIを呼び出す処理を書く ---
// const response = await openai.chat.completions.create({ ... });
return res.json({ message: 'AIからの回答(正常処理)' });
});
app.listen(3000, () => {
console.log('サーバーがポート3000で安全に稼働中です!');
});
このように、APIの入り口で express.json({ limit: '10kb' }) のようにリクエスト自体のサイズを物理的に制限しつつ、アプリケーション側でもトークン数を計算して弾くのが、実務における第一歩であり最強の防衛線になります。
防衛策②:レートリミットを実装する(短期間の連打を防ぐ)
もう一つの対策は、同じユーザーから短時間に何回もリクエストが送られてこないようにする「レートリミット(回数制限)」です。
例えば、「1分間に10回まで」といった制限を設けることで、自動化ツール(ボット)による連続攻撃を無力化できます。
Node.jsであれば、express-rate-limit という便利なライブラリを使って、以下のように簡単に設定できます。
const rateLimit = require('express-rate-limit');
// レートリミッターの設定
const aiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1分間のウィンドウ
max: 10, // 各IPアドレスから1分間に最大10回までのリクエストに制限
message: {
error: 'リクエストが多すぎます。しばらく時間を置いてから再度お試しください。'
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// AIのエンドポイントにレートリミッターを適用する
app.post('/api/chat', aiLimiter, (req, res) => {
// ここに先ほどのトークン数チェックとAI呼び出し処理が続きます
res.json({ message: '正常に処理されました' });
});
これを導入すると、ブラウザのネットワークタブ(開発者ツール)などで RateLimit-Remaining(残り何回使えるか)といったヘッダーが返されるようになります。仕組みとしても非常にスマートですよね。
—
4. 現場のプロからのアドバイス
今回は、LLMのコンテキストウィンドウを狙ったDoS攻撃の仕組みと、その防ぎ方についてお話ししました。
セキュリティ対策というと「なんだか難しそう…」「面倒くさそう…」と感じてしまうかもしれませんが、基本は「お店の入り口で、大きすぎる荷物を持った人や、短時間に何回も出入りする不審な人を優しく、でもきっちりお断りする」これだけです。
生成AIを使ったサービスを世の中にリリースするときは、動くものを作る楽しさと同じくらい、「想定外の大きなデータが来たらどうなるか?」という視点(リスク管理)をちょこっとだけ持ってあげてくださいね。
あなたの書いたコードが、ユーザーにとっても、あなた自身にとっても安全で素晴らしいものになるよう、これからも一歩ずつ楽しくセキュリティを学んでいきましょう!
コメント