目次
この記事の内容
アプリケーション開発において、セキュリティは「後付け」ではなく「設計段階」で組み込むべきものです。本記事では、OWASP Top 10 を中心に、実践的なセキュリティ設計の原則を解説します。
セキュリティ設計の核心原則
1. 防御の多層化(Defense in Depth)
一つの防御手段が破られても、他の層で防ぐ考え方です。
ユーザー入力 → バリデーション → エスケープ → 認証 → 認可 → 監査ログ
│ │ │ │ │ │
└───── 第 1 層 ─────┴── 第 2 層 ──┴─ 第 3 層 ─┘
**「一つの層が破られても、全体は守られる」**設計を目指します。
2. 最小権限の原則
ユーザー・プロセス・システムに、必要な最小限の権限だけを与えます。
| 悪い例 | 良い例 |
|---|---|
| 全ユーザーに admin 権限 | 役割ごとに権限を分離 |
| データベースユーザーに DROP 権限 | SELECT/INSERT だけ付与 |
| サービスアカウントにルート権限 | 必要リソースへのアクセスだけ許可 |
3. デフォルトで安全(Secure by Default)
設定をデフォルトで安全側にします。
# 悪い設定(デフォルトで危険)
debug: true
cors_enabled: false
session_timeout: 0 # 無期限
# 良い設定(デフォルトで安全)
debug: false
cors_enabled: true
cors_allowed_origins: ["https://example.com"]
session_timeout: 1800 # 30 分
4. 完全な仲介(Complete Mediation)
すべてのリクエストに対して認証・認可を検証します。
// 悪い例:キャッシュされた権限情報
if (user.isAdminCached) {
return deleteResource(id);
}
// 良い例:毎回検証
if (await auth.checkPermission(user.id, 'delete', resource)) {
return deleteResource(id);
}
OWASP Top 10 と対策
1. 破損したアクセス制御(Broken Access Control)
問題: 認可チェックの不備で、不正なアクセスを許可してしまう。
❌ 脆弱なコード
app.get('/user/:id/profile', (req, res) => {
const profile = db.getUser(req.params.id); // 他人の ID も取得可能
res.json(profile);
});
✅ 安全なコード
app.get('/user/profile', (req, res) => {
const profile = db.getUser(req.user.id); // ログインユーザーのみ
res.json(profile);
});
対策:
- サーバー側で認可チェック(クライアント依存禁止)
- デフォルトで拒否(ホワイトリスト方式)
- 定期的なアクセス権限の見直し
2. 暗号化の失敗(Cryptographic Failures)
問題: 機密データが平文で保存・送信される。
| データタイプ | 対策 |
|---|---|
| パスワード | bcrypt/Argon2 でハッシュ化 |
| 個人情報 | DB で暗号化(AES-256) |
| 通信 | TLS 1.2+(HTTPS) |
| セッショントークン | 暗号論的ランダム生成 |
// パスワードハッシュ化(Node.js 例)
const bcrypt = require('bcrypt');
const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);
const valid = await bcrypt.compare(password, hash);
3. インジェクション(Injection)
問題: 入力値がコードとして解釈され、不正な命令が実行される。
-- ❌ SQL インジェクション脆弱
const query = `SELECT * FROM users WHERE id = ${userId}`;
-- userId = "1 OR 1=1" → 全ユーザーが漏洩
-- ✅ パラメータ化クエリ
const query = 'SELECT * FROM users WHERE id = ?';
db.execute(query, [userId]);
対策:
- パラメータ化クエリ(プレプレアードステートメント)
- ORM/クエリビルダの使用
- 入力値のバリデーション
4. 安全でない設計(Insecure Design)
問題: 設計段階でセキュリティが考慮されていない。
対策フレームワーク:
脅威モデリング
- STRIDE モデル:Spoofing, Tampering, Repudiation, Information Disclosure, DoS, Elevation of Privilege
- 各コンポーネントの脅威を特定
セキュリティ要件の定義
- 認証要件(多要素認証の要否)
- 認可要件(RBAC, ABAC)
- 監査要件(ログ記録範囲)
セキュリティテストの計画
- 静的解析(SAST)
- 動的解析(DAST)
- 侵入テスト
5. セキュリティ設定の不備(Security Misconfiguration)
問題: デフォルト設定、不要な機能、エラーメッセージからの情報漏洩。
// ❌ 本番環境でデバッグ有効
app.use(express.debug());
// ❌ 詳細なエラーメッセージ
app.use((err, req, res, next) => {
res.status(500).send({
error: err.message,
stack: err.stack, // スタックトレース漏洩
query: req.query // クエリパラメータ漏洩
});
});
// ✅ 安全なエラーハンドリング
app.use((err, req, res, next) => {
logger.error(err); // ログには詳細
res.status(500).send({
error: 'Internal Server Error' // ユーザーには最小限
});
});
チェックリスト:
- デフォルトパスワードを変更
- 不要な機能を無効化
- エラーメッセージを一般化
- セキュリティヘッダーを設定
- 定期的な設定監査
6 脆弱なライブラリ(Vulnerable and Outdated Components)
問題: 既知の脆弱性を持つライブラリを使用。
対策:
# 依存関係の脆弱性チェック
npm audit
pip-audit
bundle-audit
# 自動化(CI に組み込み)
npm audit --audit-level=high
運用ルール:
- 依存ライブラリの Inventories を維持
- セキュリティアドバイザリの購読
- 自動化されたアップデート(Dependabot, Renovate)
7. 認証・認可の失敗(Identification and Authentication Failures)
問題: 認証プロセスの弱点を突かれる。
| 脆弱性 | 対策 |
|---|---|
| パスワード総当たり | アカウントロックアウト、レート制限 |
| セッションハイジャック | Secure, HttpOnly フラグ、定期的な再生成 |
| ブルートフォース | CAPTCHA、多要素認証 |
| パスワードリセット | 一時的トークン、有効期限付き |
// セッション管理のベストプラクティス
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
secure: true, // HTTPS のみ
httpOnly: true, // JavaScript からアクセス不可
sameSite: 'strict', // CSRF 対策
maxAge: 1800000 // 30 分
},
resave: false,
saveUninitialized: false
}));
8. ソフトウェア・データ整合性の失敗(Software and Data Integrity Failures)
問題: 改ざんされたソフトウェア・データの信頼。
対策:
- デジタル署名の検証
- サプライチェーン攻撃対策(SLSA フレームワーク)
- CI/CD パイプラインの保護
# GitHub Actions の例
permissions:
contents: read
id-token: write # OIDC 用
# デプロイ前に署名検証
- name: Verify artifact signature
run: cosign verify --key cosign.pub ${{ github.sha }}
9. 監査ログの失敗(Security Logging and Monitoring Failures)
問題: 侵害の検出・対応が遅れる。
記録すべきイベント:
- ログイン成功/失敗
- 権限変更
- データアクセス(特に機密情報)
- 入力値バリデーション失敗
- システムエラー
// 構造化ログの例
logger.warn({
event: 'LOGIN_FAILED',
userId: user.id,
ip: req.ip,
userAgent: req.get('user-agent'),
attempts: failedAttempts,
timestamp: new Date().toISOString()
});
10. サーバーサイドリクエストフォージェリ(SSRF)
問題: サーバーから内部ネットワークへ不正リクエスト。
// ❌ 脆弱なコード(ユーザー入力 URL にリクエスト)
app.get('/fetch', async (req, res) => {
const url = req.query.url;
const response = await fetch(url); // 内部 IP にもアクセス可能
res.send(response.body);
});
// ✅ 安全なコード
const ALLOWED_PROTOCOLS = ['https:'];
const BLOCKED_IPS = ['127.0.0.1', '10.', '192.168.', '172.16.'];
app.get('/fetch', async (req, res) => {
const url = new URL(req.query.url);
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
return res.status(400).send('Invalid protocol');
}
const ip = await dns.resolve(url.hostname);
if (BLOCKED_IPS.some(blocked => ip.startsWith(blocked))) {
return res.status(400).send('Access denied');
}
const response = await fetch(url);
res.send(response.body);
});
セキュリティヘッダー
HTTP レスポンスに追加すべきセキュリティヘッダー:
// Express.js 例(helmet ミドルウェア)
const helmet = require('helmet');
app.use(helmet());
// 個別設定
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"]
}
}));
| ヘッダー | 目的 |
|---|---|
| Content-Security-Policy | XSS 対策、リソース読み込み制限 |
| X-Content-Type-Options | MIME タイプスニッフィング防止 |
| X-Frame-Options | クリックジャキング防止 |
| Strict-Transport-Security | HTTPS 強制 |
| X-XSS-Protection | ブラウザ XSS フィルタ有効化 |
認証設計のパターン
1. セッション認証(传统方式)
ログイン → サーバーがセッション作成 → セッション ID を Cookie で保存
メリット: サーバー側で無効化可能、実装がシンプル デメリット: サーバーに状態が必要、スケーラビリティ課題
2. トークン認証(JWT)
ログイン → サーバーが JWT 発行 → クライアントが保存 → 各リクエストに付与
メリット: ステートレス、スケーラブル デメリット: 失効が困難、トークンサイズ大
// JWT 発行・検証例
const jwt = require('jsonwebtoken');
// 発行
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h', issuer: 'example.com' }
);
// 検証
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'example.com'
});
3. OAuth 2.0 / OIDC
サードパーティ認証やシングルサインオン(SSO)に使用。
フローの種類:
- 認証コードフロー(ウェブアプリ)
- PKCE(ネイティブアプリ、SPA)
- クライアントクレデンシャル(サーバー間)
入力値バリデーション
原則: 「入力値はすべて悪意があるとみなす」
const { z } = require('zod');
// スキーマ定義
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8).regex(/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
name: z.string().min(1).max(50),
age: z.number().int().min(0).max(150)
});
// 検証
try {
const userData = CreateUserSchema.parse(req.body);
// 安全に処理
} catch (error) {
res.status(400).json({ error: 'Invalid input' });
}
バリデーション項目:
- 型チェック(文字列、数値、配列)
- 長さ制限
- 形式チェック(メール、URL)
- 範囲チェック(数値、日付)
- 許容値リスト(enum)
セキュリティ設計チェックリスト
設計レビューで使用できるチェックリスト:
認証・認可
- パスワードはハッシュ化(bcrypt/Argon2)
- 多要素認証を実装
- セッションタイムアウトを設定
- 権限チェックはサーバー側で
- 最小権限の原則を適用
データ保護
- 通信は TLS(HTTPS)
- 機密データは暗号化
- パスワードフィールドは logging 除外
- 個人情報の保存期間を定義
入力・出力
- 入力値バリデーション
- 出力エスケープ(XSS 対策)
- パラメータ化クエリ(SQL インジェクション対策)
- ファイルアップロードは種類・サイズ制限
監視・運用
- セキュリティログを記録
- アラート閾値を設定
- インシデント対応手順を策定
- 定期的な脆弱性診断
まとめ
セキュリティ設計の核心は:
- 多層防御——一つの防御が破られても全体は守る
- デフォルトで安全——設定は安全側に倒す
- 最小権限——必要な権限だけを与える
- 完全な仲介——すべてのリクエストを検証
- 継続的改善——新たな脅威に対応
「セキュリティはプロセスであって、プロダクトではない」
OWASP Top 10 を設計段階でチェックし、継続的に改善していくことが、安全なシステム構築への道です。
参考資料
免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。