Claude CodeでSSRF攻撃を防ぐ:URLバリデーション・プライベートIP遮断・Allowlist設計
SSRF(Server-Side Request Forgery)は、サーバーを踏み台にして内部ネットワークへ不正アクセスする攻撃です。Claude Codeでツールを自動生成する際、URLを受け取るAPIや外部リソースを取得する処理を実装するケースは多い。適切な防御がなければ、AWSメタデータエンドポイントや社内サービスへのアクセスを許してしまいます。
SSRFの脅威モデル
攻撃者がURLパラメータとして以下を送り込んでくるケースを想定します。
-
http://169.254.169.254/latest/meta-data/iam/security-credentials/— AWS EC2メタデータ -
http://192.168.0.1/admin— 社内ルーターの管理画面 -
http://10.0.0.1:8080/internal-api— 内部マイクロサービス - DNS Rebinding: 最初は正規IPを返し、リクエスト時に内部IPにすり替える
プライベートIPレンジの遮断
IPv4のプライベートアドレス空間をすべてブロックします。
import dns from 'dns/promises';
import net from 'net';
const PRIVATE_RANGES = [
{ start: '10.0.0.0', end: '10.255.255.255' }, // RFC1918 Class A
{ start: '172.16.0.0', end: '172.31.255.255' }, // RFC1918 Class B
{ start: '192.168.0.0', end: '192.168.255.255' }, // RFC1918 Class C
{ start: '169.254.0.0', end: '169.254.255.255' }, // Link-local (AWS metadata)
{ start: '127.0.0.0', end: '127.255.255.255' }, // Loopback
{ start: '0.0.0.0', end: '0.255.255.255' }, // This network
{ start: '100.64.0.0', end: '100.127.255.255' }, // CGNAT
];
function ipToNumber(ip: string): number {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
}
export function isPrivateIP(ip: string): boolean {
if (!net.isIPv4(ip)) return true; // IPv6は別途対応が必要なため一律ブロック
const num = ipToNumber(ip);
return PRIVATE_RANGES.some(
({ start, end }) => num >= ipToNumber(start) && num <= ipToNumber(end)
);
}
DNS Rebinding攻撃の防止
DNS解決後に再度IPをチェックします。最初の名前解決と実際のHTTPリクエスト間にIPが変わる攻撃を防ぎます。
async function resolveAndValidate(hostname: string): Promise<string[]> {
let addresses: string[];
try {
const result = await dns.resolve4(hostname);
addresses = result;
} catch {
throw new SSRFError(`DNS解決失敗: ${hostname}`);
}
for (const ip of addresses) {
if (isPrivateIP(ip)) {
throw new SSRFError(
`プライベートIPアドレスへのアクセスは禁止されています: ${ip} (${hostname})`
);
}
}
return addresses;
}
SSRFValidatorクラスの全実装
import { URL } from 'url';
class SSRFError extends Error {
constructor(message: string) {
super(message);
this.name = 'SSRFError';
}
}
interface SSRFValidatorOptions {
allowedHosts?: string[]; // Allowlistホスト名
allowedProtocols?: string[]; // デフォルト: ['https:']
maxRedirects?: number; // リダイレクト追跡の上限
}
export class SSRFValidator {
private allowedHosts: Set<string>;
private allowedProtocols: Set<string>;
private maxRedirects: number;
constructor(options: SSRFValidatorOptions = {}) {
this.allowedHosts = new Set(options.allowedHosts ?? []);
this.allowedProtocols = new Set(options.allowedProtocols ?? ['https:']);
this.maxRedirects = options.maxRedirects ?? 5;
}
async validate(rawUrl: string): Promise<void> {
// 1. URL構文チェック
let parsed: URL;
try {
parsed = new URL(rawUrl);
} catch {
throw new SSRFError(`無効なURL形式: ${rawUrl}`);
}
// 2. プロトコルチェック(HTTPS only)
if (!this.allowedProtocols.has(parsed.protocol)) {
throw new SSRFError(
`許可されていないプロトコル: ${parsed.protocol}。使用可能: ${[...this.allowedProtocols].join(', ')}`
);
}
// 3. Allowlistチェック(設定されている場合)
if (this.allowedHosts.size > 0 && !this.allowedHosts.has(parsed.hostname)) {
throw new SSRFError(`許可リスト外のホスト: ${parsed.hostname}`);
}
// 4. IPアドレス直指定チェック
if (net.isIPv4(parsed.hostname)) {
if (isPrivateIP(parsed.hostname)) {
throw new SSRFError(`プライベートIPへの直接アクセス禁止: ${parsed.hostname}`);
}
return; // IPアドレス直指定でパブリックIPなら許可
}
// 5. DNS解決 + IPチェック(Rebinding対策)
await resolveAndValidate(parsed.hostname);
}
async safeFetch(
rawUrl: string,
options: RequestInit = {}
): Promise<Response> {
await this.validate(rawUrl);
// リダイレクト追跡(各リダイレクト先も再検証)
let currentUrl = rawUrl;
let redirectCount = 0;
while (redirectCount <= this.maxRedirects) {
const response = await fetch(currentUrl, {
...options,
redirect: 'manual', // リダイレクトを手動制御
});
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location');
if (!location) throw new SSRFError('リダイレクト先URLが不明');
// リダイレクト先も検証
const nextUrl = new URL(location, currentUrl).toString();
await this.validate(nextUrl); // 再検証
currentUrl = nextUrl;
redirectCount++;
continue;
}
return response;
}
throw new SSRFError(`リダイレクト回数上限超過: ${this.maxRedirects}回`);
}
}
使い方
const validator = new SSRFValidator({
allowedHosts: ['api.github.com', 'hooks.slack.com'],
allowedProtocols: ['https:'],
maxRedirects: 3,
});
// 安全なフェッチ(プライベートIPやAllowlist外は例外)
try {
const response = await validator.safeFetch('https://api.github.com/repos/foo/bar');
const data = await response.json();
} catch (err) {
if (err instanceof SSRFError) {
console.error('SSRF検知:', err.message);
// 403を返すなど適切なエラーハンドリング
}
}
Claude Codeへの指示パターン
Claude Codeに実装を依頼する際は以下の形式が効果的です。
外部URLを受け取るAPIエンドポイントを実装してください。
要件:
- プライベートIPレンジ(RFC1918 + AWS metadata 169.254.0.0/16)を遮断
- HTTPS onみ許可
- リダイレクト先も再検証
- Allowlist: api.github.com, hooks.slack.com のみ
SSRFValidatorクラスとして実装し、safeFetchメソッドで使えるようにすること。
まとめ
- プライベートIPレンジをすべて列挙して遮断:RFC1918 + AWS metadata (169.254.x.x) + Loopback
- DNS解決後にIPを再検証:DNS Rebinding攻撃をHTTPリクエスト直前のIPチェックで防ぐ
-
HTTPS onlyを強制:
http:やfile:プロトコルによる意図しないアクセスを排除 - リダイレクト追跡時も再検証:各リダイレクト先URLをSSRFValidatorに通してから進む
Security Pack ¥1,480 — /security-check を含む5プロンプトセット。OWASP Top 10対応のセキュリティレビューをClaude Codeで自動化できます。
prompt-works.jp で販売中。
Top comments (0)