Webアプリケーションを開発する上で、セキュリティは避けて通れない重要なテーマです。OWASP(Open Web Application Security Project)は、Webセキュリティのリスクを体系的に整理した「Top 10」を定期的に公開しています。本記事では、最も危険な脅威とその具体的な対策を、実際のコード例とともに解説します。
本記事のコード例は教育目的です。実際の攻撃に利用することは法律で禁止されています。セキュリティ知識は防御のために活用してください。
1. OWASPとTop 10とは
OWASPは、Webアプリケーションのセキュリティ向上を目的とした非営利団体です。OWASP Top 10は、最も深刻なWebセキュリティリスクを10項目にまとめたもので、世界中の開発者・セキュリティエンジニアが参照する事実上の標準です。
2025年版 OWASP Top 10 概要
- A01: アクセス制御の不備 — 権限チェックの漏れ
- A02: 暗号化の失敗 — データ暗号化の不適切な実装
- A03: インジェクション — SQL/NoSQL/OSコマンドインジェクション
- A04: 安全でない設計 — アーキテクチャレベルの脆弱性
- A05: セキュリティ設定のミス — デフォルト設定の放置
- A06: 脆弱なコンポーネント — 古いライブラリの使用
- A07: 認証の不備 — パスワード管理や認証フローの問題
- A08: ソフトウェアとデータの整合性 — CI/CDやアップデートの脆弱性
- A09: セキュリティログの不足 — 監視・ログ記録の欠如
- A10: SSRF — サーバーサイドリクエストフォージェリ
2. インジェクション攻撃
最も古典的かつ危険な攻撃手法の一つがSQLインジェクションです。ユーザー入力を直接SQLクエリに埋め込むことで、データベースを不正に操作されます。
❌ 脆弱なコード
// 危険!ユーザー入力を直接SQL文に結合
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 攻撃者が username に ' OR '1'='1' -- と入力すると…
const query = `SELECT * FROM users
WHERE username = '${username}'
AND password = '${password}'`;
db.query(query); // 全ユーザー情報が漏洩!
});
✅ 安全なコード(パラメータバインド)
// プリペアドステートメントを使用
app.post('/login', (req, res) => {
const { username, password } = req.body;
const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.query(query, [username, password]); // 安全!
});
ORMを使用するとSQLインジェクションのリスクを大幅に軽減できます。Prisma、Sequelize、TypeORMなどのORMを積極的に活用しましょう。
3. 認証の不備
認証機能は多くのアプリケーションの要です。よくある脆弱性と対策を見てみましょう。
よくある認証の問題
- 弱いパスワードポリシー:「password123」を許可してしまう
- ブルートフォース対策なし:ログイン試行回数の制限がない
- セッション管理の不備:セッションIDの予測可能性
- JWTの不適切な使用:alg=noneの許可、秘密鍵の漏洩
安全な認証の実装例
import bcrypt from 'bcrypt';
import rateLimit from 'express-rate-limit';
// ログイン試行回数の制限
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分間
max: 5, // 最大5回
message: 'ログイン試行回数を超えました。15分後に再試行してください。'
});
app.post('/login', loginLimiter, async (req, res) => {
const { email, password } = req.body;
const user = await User.findByEmail(email);
if (!user) {
// タイミング攻撃対策:ユーザーが存在しなくても同じ時間で応答
await bcrypt.compare(password, '$2b$10$invalidhashplaceholder');
return res.status(401).json({ error: '認証に失敗しました' });
}
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({ error: '認証に失敗しました' });
}
// セッション作成(安全な設定)
req.session.userId = user.id;
req.session.cookie.httpOnly = true;
req.session.cookie.secure = true;
req.session.cookie.sameSite = 'strict';
});
4. クロスサイトスクリプティング(XSS)
XSS攻撃は、悪意のあるスクリプトをWebページに注入し、他のユーザーのブラウザで実行させる攻撃です。
XSSの3つのタイプ
- Stored XSS:悪意のあるスクリプトがサーバーに保存される(掲示板、コメント欄)
- Reflected XSS:URLパラメータが反映されるページで発生
- DOM-based XSS:JavaScriptが動的にDOMを操作する際に発生
XSS対策
// ❌ 危険:innerHTMLでユーザー入力を挿入
element.innerHTML = userInput;
// ✅ 安全:textContentを使用
element.textContent = userInput;
// ✅ サーバーサイドでのエスケープ
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// ✅ Content Security Policy(CSP)ヘッダーの設定
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
next();
});
5. CSRF攻撃と対策
CSRF(Cross-Site Request Forgery)は、ユーザーが意図しないリクエストを、認証済みのセッションを利用して送信させる攻撃です。
import csrf from 'csurf';
// CSRFトークンの設定
const csrfProtection = csrf({ cookie: true });
app.get('/transfer', csrfProtection, (req, res) => {
res.render('transfer', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// CSRFトークンが有効な場合のみ処理が実行される
processTransfer(req.body);
});
6. セキュリティチェックリスト
Webアプリケーションをリリースする前に、以下のチェックリストを確認しましょう:
- ☑️ すべてのユーザー入力をバリデーション・サニタイズしているか
- ☑️ パラメータバインドを使用しているか(SQLインジェクション対策)
- ☑️ パスワードをbcrypt/argon2でハッシュ化しているか
- ☑️ HTTPSを強制しているか
- ☑️ CSPヘッダーを設定しているか
- ☑️ CSRFトークンを使用しているか
- ☑️ セッションCookieにHttpOnly, Secure, SameSiteを設定しているか
- ☑️ 依存パッケージの脆弱性を定期チェックしているか(
npm audit) - ☑️ エラーメッセージに機密情報を含めていないか
- ☑️ ログに不正アクセスの試行を記録しているか
セキュリティは「一度対策すれば終わり」ではありません。新しい脆弱性は常に発見されるため、継続的な学習とアップデートが不可欠です。まずはこの記事で紹介した基本的な対策から始めて、徐々にセキュリティ知識を深めていきましょう。