職場でChatGPTを使い始めた人から「これ、情報漏洩しないの?」と聞かれた。
「たぶん大丈夫」と答えたが、正直よくわかっていなかった。調べてみたら、LLM(大規模言語モデル)専用のセキュリティフレームワーク「OWASP LLM Top 10」というものがあった。
で、これを診断フォームにしたら面白いかもと思って作った。完成したもの: AIセキュリティリスク診断
この記事はその実装記録だ。「LLMセキュリティって何があるの?」という人の役にも立てばいいなと思っている。
この記事でできること
- LLM(大規模言語モデル)特有のセキュリティリスク6種類を理解できる
- OWASP LLM Top 10 2025 の主要リスクをコードに落とし込む考え方がわかる
- 22問の診断フォームをVanilla JSで実装する手順がわかる
- 実際に動くツールはAIセキュリティリスク診断で確認できる
環境・前提
- ブラウザ: モダンブラウザ(Chrome/Firefox/Safari最新版)
- JavaScript: Vanilla JS(ES2020)。フレームワーク不使用
- D3.js: v7(レーダーチャート描画用。動的ロードで読み込む)
- CMS: WordPress(SWELLテーマ。外部scriptタグが動かない制約あり)
- 追加依存: なし
完成形
22問に答えると、6軸(データ/モデル/運用/ガバナンス/サプライチェーン/コンプライアンス)のレーダーチャートと業界ベンチマーク比較が出る。NIST AI RMF、OWASP LLM Top 10、総務省ガイドラインを参照した設問になっている。
動くものを先に見たい人: AIセキュリティリスク診断
手順
Step 1: LLMセキュリティの6軸を設計する
まず「何を診断するか」を決める必要がある。
OWASP LLM Top 10 2025を見ると、LLM固有のリスクが10個ある。
- LLM01: プロンプトインジェクション(最重要)
- LLM02: 安全でない出力の取り扱い
- LLM03: 訓練データの汚染
- LLM05: サプライチェーンの脆弱性
- LLM06: 機密情報の漏洩
- LLM08: 過度な権限付与(AIエージェントに広すぎる権限を渡す)
- LLM10: モデルの窃盗
10個をそのまま10軸にすると設問が多すぎる。似た概念をまとめて6軸にした。
const AXES = [
{ id: 'data', label: 'データセキュリティ', questions: ['q1','q2','q3','q4'] },
{ id: 'model', label: 'モデルセキュリティ', questions: ['q5','q6','q7'] },
{ id: 'operation', label: '運用セキュリティ', questions: ['q8','q9','q10','q11'] },
{ id: 'governance', label: 'ガバナンス', questions: ['q12','q13','q14','q15'] },
{ id: 'supply', label: 'サプライチェーン', questions: ['q16','q17','q18','q19'] },
{ id: 'compliance', label: 'コンプライアンス', questions: ['q20','q21','q22'] }
];
つまずきポイント: 最初はOWASP LLM Top 10の番号順に軸を作ろうとした。でも「ユーザーが答えやすい順序」と「フレームワークの順序」は別物だった。一般のビジネスマンにとって「データ管理→AIシステム→日常運用→ルール整備」の順番のほうが自然に答えられる。
Step 2: 設問データをJSONで定義する
設問はJavaScriptのオブジェクトとして管理する。後からメンテナンスしやすくするためだ。
const QUESTIONS = {
q1: {
axis: 'data',
text: 'AI学習データ・入力データに含まれる個人情報・機密情報を分類・管理していますか?',
ref: 'AI事業者ガイドライン 1.2版'
},
q8: {
axis: 'operation',
text: 'プロンプトインジェクション対策(入出力フィルタリング等)を実装していますか?',
ref: 'OWASP LLM01'
},
// ... 22問分
};
ポイント: ref(参照フレームワーク)を各設問に持たせておくと、結果画面で「この質問はどの規制に基づいているか」を表示できる。
Step 3: 4段階スコアリングを実装する
「やっている」「やっていない」の2択では粒度が荒い。4段階にした。
const CHOICES = [
{ value: 3, label: '十分に実施している' },
{ value: 2, label: '一部実施している' },
{ value: 1, label: '検討中・計画段階' },
{ value: 0, label: '未対応' }
];
function calcAxisScore(axisId, answers) {
const axis = AXES.find(a => a.id === axisId);
const maxScore = axis.questions.length * 3;
const rawScore = axis.questions.reduce(function(sum, qid) {
const val = answers[qid];
return sum + (val !== undefined ? val : 0);
}, 0);
return Math.round((rawScore / maxScore) * 100);
}
つまずきポイント: && 演算子をWordPressの本番環境で使うと、&& にHTMLエンティティ変換されてJSが壊れる。三項演算子か || で代替する。上記コードの (val !== undefined ? val : 0) はそのための対処だ。
Step 4: レーダーチャートをD3.jsで描画する
レーダーチャートはD3.jsで実装する。Chart.jsではWordPressで動かなかったので。
function loadD3(callback) {
if (typeof d3 !== 'undefined') {
callback();
return;
}
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/d3@7';
s.onload = callback;
document.head.appendChild(s);
}
function renderRadar(scores) {
loadD3(function() {
const axes = Object.keys(scores);
const n = axes.length;
const angleSlice = (Math.PI * 2) / n;
const radius = 150;
// 各軸の座標を計算
function getCoord(axisIndex, value) {
const angle = angleSlice * axisIndex - Math.PI / 2;
const r = (value / 100) * radius;
return {
x: r * Math.cos(angle),
y: r * Math.sin(angle)
};
}
// ポリゴン描画
const points = axes.map(function(axis, i) {
const coord = getCoord(i, scores[axis]);
return coord.x + ',' + coord.y;
}).join(' ');
svg.append('polygon')
.attr('points', points)
.attr('fill', 'rgba(255, 137, 6, 0.3)')
.attr('stroke', 'var(--aji-accent)')
.attr('stroke-width', 2)
.attr('transform', 'translate(200, 200)');
});
}
つまずきポイント: <script src="CDN"> タグをHTMLに直書きしても、SWELLテーマはカスタムHTMLブロック内の外部スクリプトを無視する。document.createElement('script') で動的ロードするしかない。
Step 5: スマホでのタッチ対応を実装する
スマホでグラフ要素をタップしたとき、ツールチップが画面外に出て読めない問題が起きた。
画面下部に固定パネルを作って表示する方法に変えた。
function showMobilePanel(title, score, detail) {
var panel = document.querySelector('.aji-ai-security-risk-check-mobile-panel');
if (!panel) return;
panel.querySelector('.panel-title').textContent = title;
panel.querySelector('.panel-score').textContent = score + 'pt';
panel.querySelector('.panel-detail').textContent = detail;
panel.style.display = 'block';
}
// タッチイベントに stopPropagation を必ず付ける
radarPolygon.addEventListener('touchstart', function(e) {
e.preventDefault();
e.stopPropagation(); // これがないとパネルが開いて即閉じる
showMobilePanel(axisLabel, score, actionText);
});
つまずきポイント: stopPropagation() を付けないと、touchstartがdocumentまで伝播して「パネル外タップ=閉じる」の判定が誤発火する。必ずセットで書く。
Step 6: 結果のランク判定と改善アクションを表示する
総合スコアからランクを判定し、軸ごとの改善アクションを表示する。
function getOverallRank(totalScore) {
if (totalScore >= 85) return { rank: 'A', label: '先進レベル' };
if (totalScore >= 68) return { rank: 'B', label: '標準レベル' };
if (totalScore >= 52) return { rank: 'C', label: '要注意' };
if (totalScore >= 35) return { rank: 'D', label: '危険' };
return { rank: 'E', label: '緊急対応必要' };
}
// 軸別の改善アクション(スコアが50以下の軸に表示)
const ACTIONS = {
data: 'AI利用時の入力禁止事項(個人情報・社内機密等)のポリシーを文書化し、全従業員に周知する',
model: 'LLM APIのアクセスキーをチームで共有せず、個人ごとに発行して定期ローテーションを実施する',
operation: '生成AIの出力を重要判断に使う前に、必ず人間がファクトチェックするルールを定める',
governance: 'AIの利用目的・禁止事項・責任者を明記したAI利用ポリシーを1枚紙で策定する',
supply: '外部LLMサービス(ChatGPT、Claude等)の利用規約のデータ取り扱い条項を確認・記録する',
compliance: 'AI関連インシデント発生時の報告先・封じ込め手順・復旧ステップを1ページにまとめる'
};
ポイント: 改善アクションは「今日から始められる最初の一手」に限定した。「完全なガバナンス体制を構築する」ではなく「1枚紙でポリシーを作る」という具体性が大切だ。
つまずきポイントまとめ
-
&&演算子: WordPressで&&に変換されてJSが壊れる。三項演算子か||を使う -
<script src>: SWELLテーマのカスタムHTMLブロックは外部スクリプトを無視する。createElement('script')で動的ロード - スマホのツールチップ: マウス追従型は画面外に出る。下部固定パネル方式に変える
-
stopPropagation()の忘れ: タッチイベントのバブリングでパネルが即閉じする。e.stopPropagation()を必ず書く - 設問の順序: フレームワークの番号順と「答えやすい順序」は別物。ユーザー視点で並べ直す
引用・出典
- OWASP「LLM Top 10 2025」2025年公表(https://owasp.org/www-project-top-10-for-large-language-model-applications/)
- NIST「AI Risk Management Framework 1.0」2023年1月26日
- 総務省「AIのセキュリティ確保のための技術的対策に係るガイドライン」2026年3月27日
- Practical DevSecOps「AI Security Statistics 2026」2026年公表
- Accenture「Cyber Resilience 2025」2025年7月28日
