Node.js セキュリティ
1. Node.js におけるセキュリティの重要性
Node.js アプリケーションにとって、セキュリティはいくつかの重要な理由から極めて重要です。
- JavaScript エコシステムの規模: npm レジストリには 150 万を超えるパッケージが存在し、すべての依存関係(Dependencies)の安全性を検証することは困難です。
- サーバーサイドでの実行: クライアントサイドの JavaScript とは異なり、Node.js はファイルシステムやネットワーク、その他の機密リソースに直接アクセスできます。
- デフォルトの寛容さ: Node.js はデフォルトではセキュリティ制限がほとんどないため、セキュアなコーディングの実践が不可欠です。
- イベント駆動型アーキテクチャ: 非同期操作(Asynchronous operations)は複雑な実行フローを生み出し、セキュリティ上の欠陥を隠してしまう可能性があります。
アプリケーションが侵害された場合、攻撃者は以下の行動をとる可能性があります。
- 機密性の高いユーザーデータへのアクセス
- アプリケーションの挙動の操作
- サーバーを使用した暗号通貨のマイニング
- 他のシステムへの攻撃の踏み台にする
- 組織の信頼・評判の毀損
2. Node.js における一般的なセキュリティ脆弱性
| 脆弱性 | 説明 | 影響 |
|---|---|---|
| インジェクション攻撃 | アプリケーションが処理する入力に悪意のあるコードを挿入する(SQL, NoSQL, OS コマンド) | データの盗難、不正アクセス、サービスの中断 |
| クロスサイトスクリプティング (XSS) | 他のユーザーが閲覧するウェブページにクライアントサイドのスクリプトを注入する | セッションハイジャック、資格情報の盗難、改ざん |
| 認証の不備 | 資格情報の漏洩を許す認証メカニズムの欠陥 | アカウントの乗っ取り、権限昇格 |
| 安全でない依存関係 | 既知の脆弱性を持つサードパーティパッケージの使用 | 依存関係からすべての脆弱性を継承する |
| 情報の露出 | エラーメッセージ、ログ、またはレスポンスを通じた機密データの漏洩 | システム情報の開示、データ漏洩 |
| クロスサイトリクエストフォージェリ (CSRF) | ユーザーを騙して、認証済みのウェブアプリケーション上で意図しないアクションを実行させる | ユーザーに代わって不正な操作を実行する |
| セキュリティ設定のミス | アプリケーションにおけるセキュリティ設定の不適切な構成 | さまざまなセキュリティギャップと脆弱性 |
| パストラバーサル | 意図したアプリケーションパス以外のファイルやディレクトリへのアクセス | 不正なファイルアクセス、コードの実行 |
3. 必須のセキュリティ・ベストプラクティス
3.1 入力のバリデーションとサニタイズ
ユーザーの入力を決して信頼してはいけません。アプリケーションの外部から来るすべてのデータは、常にバリデーション(検証)とサニタイズ(浄化)を行ってください。
例:Express-Validator を使用した入力バリデーション
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();
app.use(express.json());
// バリデーションルールの定義
const userValidationRules = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('age').isInt({ min: 18 }).toInt(),
body('name').trim().escape().notEmpty()
];
// バリデーションの適用
app.post('/register', userValidationRules, (req, res) => {
// バリデーションエラーのチェック
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// バリデーション済みデータの処理
const { email, password, age, name } = req.body;
// ... バリデーション済みデータを安全に使用できます
res.status(201).json({ message: 'ユーザーが正常に登録されました' });
});3.2 インジェクション攻撃の防止
パラメータ化クエリ(Parameterized queries)を使用し、ユーザー入力の直接的な連結を避けることで、SQL、NoSQL、コマンドインジェクションなどを防ぎます。
例:SQL インジェクションの防止
// 脆弱な例 - 使用しないでください
function searchUsersUnsafe(name) {
// 文字列の直接連結 - インジェクションに対して脆弱
return db.query(`SELECT * FROM users WHERE name LIKE '%${name}%'`);
}
// 安全な例 - このアプローチを使用してください
function searchUsersSafe(name) {
// パラメータ化クエリ - インジェクションから保護
return db.query('SELECT * FROM users WHERE name LIKE ?', [`%${name}%`]);
}3.3 クロスサイトスクリプティング (XSS) の防止
出力を適切にエンコードし、コンテンツセキュリティポリシー (CSP) を使用することで XSS から保護します。
例:XSS の防止
const express = require('express');
const app = express();
// 脆弱な例 - ユーザー入力を HTML に直接挿入
app.get('/unsafe', (req, res) => {
const userInput = req.query.message || '';
res.send(`<div>あなたのメッセージ: ${userInput}</div>`);
});
// 安全な例 - ユーザー入力のエンコード
app.get('/safe', (req, res) => {
const userInput = req.query.message || '';
// HTML 特殊文字をエンコード
const safeInput = userInput
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
res.send(`<div>あなたのメッセージ: ${safeInput}</div>`);
});3.4 依存関係を最新に保つ
npm audit やその他のセキュリティツールを使用して、脆弱な依存関係を定期的にチェックし、更新してください。
脆弱性のチェックコマンド
# 脆弱な依存関係をチェック
npm audit
# 可能な場合は自動的に脆弱性を修正
npm audit fix
# プロダクション環境の依存関係のみチェック
npm audit --production
# 詳細なレポートを生成
npm audit --json > audit-report.json3.5 セキュアな認証の実践
適切なパスワードハッシュ化、アカウントロックアウト、多要素認証(MFA)を使用して認証を安全に実装します。
例:セキュアなパスワードハッシュ化
const crypto = require('crypto');
// ランダムなソルト(Salt)を生成
function generateSalt() {
return crypto.randomBytes(16).toString('hex');
}
// PBKDF2 でパスワードをハッシュ化
function hashPassword(password, salt) {
return crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
}
// パスワードを安全に保存して新規ユーザーを登録
function registerUser(username, password) {
// このユーザー専用のユニークなソルトを生成
const salt = generateSalt();
// ソルトを使用してパスワードをハッシュ化
const hashedPassword = hashPassword(password, salt);
// ユーザー名、hashedPassword、salt をデータベースに保存
// プレーンテキスト(生パスワード)は絶対に保存しないでください
return { username, hashedPassword, salt };
}
// ログイン試行の検証
function verifyUser(username, password, storedHash, storedSalt) {
// 入力されたパスワードを保存されているソルトでハッシュ化
const hashedAttempt = hashPassword(password, storedSalt);
// タイミング攻撃を防ぐために一定時間比較(Time-constant comparison)を使用
return crypto.timingSafeEqual(
Buffer.from(hashedAttempt, 'hex'),
Buffer.from(storedHash, 'hex')
);
}3.6 セキュリティヘッダーの使用
HTTP セキュリティヘッダーを実装して、さまざまな攻撃から保護します。Helmet.js などのパッケージを使用すると、これを簡単に実現できます。
例:Helmet.js の使用
const express = require('express');
const helmet = require('helmet');
const app = express();
// デフォルト設定ですべてのセキュリティヘッダーを適用
app.use(helmet());
// または特定のヘッダーをカスタマイズ
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", 'trusted-cdn.com']
}
},
// クリックジャッキングを防止
frameguard: { action: 'deny' },
// Strict-Transport-Security (HSTS)
hsts: { maxAge: 15552000, includeSubDomains: true }
}));3.7 HTTPS の使用
プロダクション環境では常に HTTPS を使用し、転送中のデータを暗号化してください。
例:Express での HTTPS セットアップ
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
// ルート定義
app.get('/', (req, res) => {
res.send('セキュアな HTTPS サーバー');
});
// HTTPS 設定
const options = {
key: fs.readFileSync('path/to/private-key.pem'),
cert: fs.readFileSync('path/to/certificate.pem'),
// モダンで安全な TLS オプション
minVersion: 'TLSv1.2',
ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256'
};
// HTTPS サーバーの作成
https.createServer(options, app).listen(443, () => {
console.log('HTTPS サーバーがポート 443 で起動しました');
});3.8 機密データの保護
環境バリアブルや専用のシークレット管理ソリューションを使用して、機密データを安全に保存します。
例:環境バリアブルの使用
// 開発環境では .env ファイルからロード
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
// 環境バリアブルにアクセス
const dbConnection = {
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
};
// 機密情報は絶対にログに出力しないでください
console.log('データベースに接続完了:', dbConnection.host);
// NG例: console.log('データベース接続情報:', dbConnection); 重要: 機密データをバージョンコントロールにコミットしないでください。.gitignore を使用して .env ファイルを除外してください。
4. 依存関係の脆弱性管理
Node.js アプリケーションは通常、多数の依存関係を持っており、それぞれがセキュリティ脆弱性を持ち込む可能性があります。適切な依存関係管理は、アプリケーションの安全性を維持するために不可欠です。
4.1 npm audit の活用
npm audit コマンドは、依存関係ツリーをスキャンし、既知の脆弱性を持つパッケージを特定します。
# 基本的な診断を実行
npm audit
# 可能な場合に脆弱性を自動修正
npm audit fix
# メジャーバージョンの更新が必要な脆弱性も強制的に修正
npm audit fix --forcenpm audit の出力には以下の内容が含まれます。
- 脆弱性の深刻度 (Low, Moderate, High, Critical)
- 影響を受けるパッケージと脆弱なバージョンの範囲
- 脆弱性の説明
- 脆弱な依存関係へのパス
- 問題を解決するための推奨アクション
4.2 脆弱性防止戦略
- 依存関係のロック:
package-lock.jsonまたはyarn.lockを使用して、依存関係のバージョンを固定します。 - 最小バージョンの設定: バージョン範囲には下限を設定します (例:
"express": "^4.17.1")。 - 自動スキャン: セキュリティスキャンを CI/CD パイプラインに統合します。
- 代替案の検討: 問題のあるパッケージについては、より安全な実績のある代替パッケージを調査します。
4.3 サードパーティのセキュリティツール
| ツール | 目的 |
|---|---|
| Snyk | 依存関係のスキャン、自動修正 PR の提供、継続的なモニタリング |
| SonarQube | コード内の脆弱性、コードの「臭い (Code smell)」、メンテナンス性の問題を検出 |
| OWASP Dependency-Check | 既知の脆弱性を持つプロジェクト依存関係を特定 |
| WhiteSource Bolt | オープンソースコンポーネントの継続的なセキュリティとコンプライアンス管理 |
5. アドバンスド・セキュリティ・プラクティス
5.1 レート制限 (Rate Limiting)
レート制限を実装して、API を悪用や総当たり攻撃(Brute force attacks)から保護します。
例:Express-Rate-Limit によるレート制限
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// 基本的なレートリミッター: IP ごとに 15 分間で最大 100 リクエスト
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分
max: 100, // windowMs 内のリクエスト制限数
standardHeaders: true, // `RateLimit-*` ヘッダーに制限情報を返す
message: 'この IP からのリクエストが多すぎます。15 分後に再度お試しください'
});
// すべてのリクエストに適用
app.use(limiter);
// または特定のルートのみに適用
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 時間
max: 5, // 1 時間に 5 回までの失敗を許可
message: 'ログイン試行回数が多すぎます。1 時間後に再度お試しください'
});
app.post('/login', loginLimiter, (req, res) => {
// ログインロジック
});5.2 CSRF 保護
CSRF トークンを実装して、クロスサイトリクエストフォージェリ攻撃を防止します。
例:csurf による CSRF 保護
const express = require('express');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const app = express();
// ミドルウェアの設定
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
// CSRF 保護の初期化
const csrfProtection = csrf({ cookie: true });
// CSRF トークンを含むフォーム表示ルート
app.get('/form', csrfProtection, (req, res) => {
res.send(`
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<input type="text" name="data">
<button type="submit">送信</button>
</form>
`);
});
// CSRF 検証を伴うフォーム送信ルート
app.post('/process', csrfProtection, (req, res) => {
// ここに到達すれば CSRF トークンは有効
res.send('データが正常に処理されました');
});
// CSRF エラーのハンドリング
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
res.status(403).send('CSRF トークンの検証に失敗しました');
} else {
next(err);
}
});5.3 コンテンツセキュリティポリシー (CSP)
CSP は、ブラウザがロードできるリソースを制御することで、XSS やデータインジェクション攻撃を防止します。
例:CSP のセットアップ
const express = require('express');
const helmet = require('helmet');
const app = express();
// 詳細な CSP 構成
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"], // 同一オリジンのリソースのみ許可
scriptSrc: ["'self'", "'unsafe-inline'", 'trusted-cdn.com'],
styleSrc: ["'self'", "'unsafe-inline'", 'trusted-cdn.com'],
imgSrc: ["'self'", 'data:', 'trusted-cdn.com', 'another-trusted-cdn.com'],
connectSrc: ["'self'", 'api.example.com'], // API エンドポイント
fontSrc: ["'self'", 'fonts.googleapis.com', 'fonts.gstatic.com'],
objectSrc: ["'none'"], // object, embed, applet 要素を禁止
mediaSrc: ["'self'"], // オーディオ、ビデオソース
frameSrc: ["'self'"], // フレーム
sandbox: ['allow-forms', 'allow-scripts', 'allow-same-origin'],
reportUri: '/csp-violation-report'
}
}));
// CSP 違反レポートを処理するルート
app.post('/csp-violation-report', (req, res) => {
console.log('CSP 違反:', req.body);
res.status(204).end();
});5.4 セキュリティロギングとモニタリング
セキュリティインシデントを検出し対応するために、包括的なロギングを実装します。
例:Winston によるセキュリティロギング
const winston = require('winston');
const express = require('express');
const app = express();
// セキュリティ専用ロガーの作成
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'security-service' },
transports: [
new winston.transports.File({ filename: 'security-events.log' })
]
});
// 認証試行のログ記録
app.post('/login', (req, res) => {
const { username } = req.body;
const ip = req.ip;
// 認証ロジック...
const success = true; // 実際のロジックに置き換え
securityLogger.info({
event: 'authentication_attempt',
username,
ip,
success,
userAgent: req.get('User-Agent')
});
});6. セキュア開発ライフサイクル (SDLC)
安全な Node.js アプリケーションを構築するには、開発プロセス全体にセキュリティを統合する必要があります。
6.1 SDLC のフェーズ別ベストプラクティス
- 要件定義 & 設計フェーズ
- セキュリティ要件とコンプライアンスの定義
- 潜在的なリスクを特定するための脅威モデリング(Threat modeling)の実施
- セキュリティ原則(最小権限の原則、防御の深層)に基づいた設計
- 実績のある安全なフレームワークとライブラリの選択
- 開発フェーズ
- セキュアなコーディング標準とリンターの使用
- 入力バリデーションと出力エンコーディングの実装
- データベースアクセスへのパラメータ化クエリの使用
- テストフェーズ
- 静的解析セキュリティテスト (SAST) の実施
- 動的解析セキュリティテスト (DAST) の実施
- 依存関係の脆弱性スキャンの実行
- ペネトレーションテスト(侵入テスト)の実施
- デプロイ & メンテナンス
- セキュアな構成管理
- 継続的なセキュリティモニタリングの実装
- インシデントレスポンス計画の策定
- 定期的なセキュリティ監査のスケジュール
例:セキュア開発チェックリスト (package.json)
{
"name": "secure-node-app",
"version": "1.0.0",
"scripts": {
"start": "node app.js",
"test": "jest",
"lint": "eslint . --ext .js",
"audit": "npm audit --production --audit-level=high",
"check-vuln": "npx snyk test",
"security-check": "npm-run-all --parallel lint audit check-vuln",
"precommit": "npm run security-check"
},
"devDependencies": {
"eslint": "^8.0.0",
"eslint-plugin-security": "^1.5.0",
"husky": "^8.0.0",
"npm-run-all": "^4.1.5",
"snyk": "^1.1000.0"
}
}ヒント: セキュリティチェックを CI/CD パイプラインに統合し、プロダクションに到達する前にセキュリティ問題を自動的に捕捉するようにしましょう。
7. まとめ
セキュリティは一度の実装で終わるものではなく、継続的なプロセスです。アプリケーションの脆弱性は常に最も弱い部分に依存することを忘れないでください。
- すべての入力をバリデーションおよびサニタイズする
- 一般的な攻撃(XSS, CSRF, インジェクション)から保護する
- 依存関係を最新に保ち、定期的に監査する
- セキュアな認証とセッション管理を実装する
- HTTPS と適切なセキュリティヘッダーを使用する
- 機密データを安全に保存し、レート制限とモニタリングを導入する
- 確立されたセキュリティガイドライン(OWASP など)に従う
定期的なセキュリティレビューとペネトレーションテストの実施を強く推奨します。