このレッスンで学ぶこと
このレッスンでは、WebSocketのセキュリティとポート管理について学習します。
- ws(ポート80)とwss(ポート443)の違いと使い分け
- Originチェックとセキュリティメカニズム
- CORSとの違いと実装上の注意点
- 企業環境でのWebSocket運用のベストプラクティス
このレッスンでは、WebSocketのセキュリティとポート管理について学習します。
WebSocketには暗号化されていないws://
と、TLS暗号化されたwss://
の2つのスキームがあります。
ws://localhost:8080/websocket
wss://api.example.com/websocket
開発環境
// ローカル開発では ws:// も許可
const websocketUrl = process.env.NODE_ENV === 'development'
? 'ws://localhost:8080/websocket'
: 'wss://api.production.com/websocket';
本番環境
// 本番環境では必ず wss:// を使用
const websocketUrl = 'wss://api.production.com/websocket';
// TLS設定の強化
const tlsOptions = {
minVersion: 'TLSv1.2',
ciphers: [
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES128-SHA256',
'ECDHE-RSA-AES256-SHA384'
].join(':'),
honorCipherOrder: true
};
1. 証明書の自動更新(Let's Encrypt)
Node.js: 18.x 以上
必要パッケージ: ws, https, fs, crypto
TLS証明書: Let's Encrypt または商用CA
実行方法: sudo node secure-websocket-server.js
const https = require('https');
const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');
class AutoRenewingWebSocketServer {
constructor() {
this.certPath = '/etc/letsencrypt/live/example.com';
this.server = null;
this.wss = null;
this.startServer();
// 証明書の定期チェック(毎日)
setInterval(() => this.checkAndRenewCertificate(), 24 * 60 * 60 * 1000);
}
loadCertificates() {
try {
return {
key: fs.readFileSync(path.join(this.certPath, 'privkey.pem')),
cert: fs.readFileSync(path.join(this.certPath, 'fullchain.pem'))
};
} catch (error) {
console.error('Failed to load certificates:', error);
throw error;
}
}
startServer() {
const credentials = this.loadCertificates();
this.server = https.createServer({
...credentials,
// セキュリティ強化設定
secureProtocol: 'TLSv1_2_method',
honorCipherOrder: true,
ciphers: [
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES128-SHA256',
'ECDHE-RSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA'
].join(':')
});
this.wss = new WebSocket.Server({
server: this.server,
verifyClient: this.verifyClient.bind(this)
});
this.server.listen(443, () => {
console.log('Secure WebSocket server started on port 443');
});
}
async checkAndRenewCertificate() {
try {
const certInfo = await this.getCertificateInfo();
const daysUntilExpiry = Math.floor(
(certInfo.validTo - Date.now()) / (1000 * 60 * 60 * 24)
);
if (daysUntilExpiry <= 30) {
console.log(`Certificate expires in ${daysUntilExpiry} days. Renewing...`);
await this.renewCertificate();
this.restartServer();
}
} catch (error) {
console.error('Certificate check failed:', error);
}
}
verifyClient(info) {
// TLS証明書の追加検証
const cert = info.req.socket.getPeerCertificate();
if (cert && cert.subject) {
console.log('Client certificate subject:', cert.subject);
// クライアント証明書ベースの認証も実装可能
}
return true; // 基本的な検証はTLS層で完了
}
}
2. Perfect Forward Secrecy (PFS) の実装
const tlsConfig = {
// PFSを提供する暗号スイートのみ使用
ciphers: [
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'DHE-RSA-AES256-GCM-SHA384'
].join(':'),
// 楕円曲線の指定
ecdhCurve: 'secp384r1:prime256v1',
// DHパラメータファイル(2048bit以上)
dhparam: fs.readFileSync('/path/to/dhparam.pem'),
// セッション再利用の制限
sessionTimeout: 300, // 5分
// HSTSヘッダーの自動設定
requestCert: false,
rejectUnauthorized: false
};
WebSocketはCSRFトークンを自動送信しないため、専用の対策が必要です。
1. CSRFトークン検証
Node.js: 18.x 以上
必要パッケージ: ws, crypto, express, cookie-parser
セッション管理: Redis または メモリストア
実行方法: node csrf-protected-server.js
class CSRFProtectedWebSocket {
constructor() {
this.csrfTokens = new Map(); // セッションID → CSRFトークン
}
// CSRFトークン生成(HTTPエンドポイント)
generateCSRFToken(sessionId) {
const token = crypto.randomBytes(32).toString('hex');
this.csrfTokens.set(sessionId, {
token,
createdAt: Date.now(),
expiresAt: Date.now() + 30 * 60 * 1000 // 30分で期限切れ
});
return token;
}
// WebSocket接続時のCSRF検証
verifyCSRFToken(request) {
// URLからCSRFトークンを取得
const url = new URL(request.url, 'ws://localhost');
const providedToken = url.searchParams.get('csrf_token');
const sessionId = this.extractSessionId(request);
if (!providedToken || !sessionId) {
throw new Error('CSRF token or session ID missing');
}
const storedTokenData = this.csrfTokens.get(sessionId);
if (!storedTokenData) {
throw new Error('Invalid session');
}
if (storedTokenData.expiresAt < Date.now()) {
this.csrfTokens.delete(sessionId);
throw new Error('CSRF token expired');
}
if (storedTokenData.token !== providedToken) {
throw new Error('Invalid CSRF token');
}
// 使用済みトークンは削除(再利用防止)
this.csrfTokens.delete(sessionId);
return true;
}
extractSessionId(request) {
// Cookieからセッション IDを抽出
const cookies = request.headers.cookie;
if (!cookies) return null;
const sessionMatch = cookies.match(/sessionId=([^;]+)/);
return sessionMatch ? sessionMatch[1] : null;
}
}
// 使用例
const csrfProtection = new CSRFProtectedWebSocket();
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info) => {
try {
csrfProtection.verifyCSRFToken(info.req);
return true;
} catch (error) {
console.log('CSRF verification failed:', error.message);
return false;
}
}
});
2. Originチェックの強化
class EnhancedOriginChecker {
constructor() {
this.allowedOrigins = new Set([
'https://myapp.example.com',
'https://admin.example.com'
]);
this.blockedIPs = new Set();
this.suspiciousActivity = new Map();
}
verifyOriginWithThreatDetection(request) {
const origin = request.headers.origin;
const clientIP = this.getClientIP(request);
// ブロックされたIPのチェック
if (this.blockedIPs.has(clientIP)) {
throw new Error('IP address is blocked');
}
// Originの基本検証
if (!origin) {
this.recordSuspiciousActivity(clientIP, 'missing_origin');
throw new Error('Origin header is required');
}
if (!this.allowedOrigins.has(origin)) {
this.recordSuspiciousActivity(clientIP, 'invalid_origin', { origin });
throw new Error(`Origin not allowed: ${origin}`);
}
// 追加のセキュリティチェック
this.validateOriginSyntax(origin);
this.checkForSuspiciousPatterns(origin, clientIP);
return true;
}
recordSuspiciousActivity(ip, type, details = {}) {
const key = `${ip}:${type}`;
const activity = this.suspiciousActivity.get(key) || {
count: 0,
firstSeen: Date.now(),
lastSeen: Date.now()
};
activity.count++;
activity.lastSeen = Date.now();
activity.details = details;
this.suspiciousActivity.set(key, activity);
// 閾値を超えた場合はIPをブロック
if (activity.count >= 5) {
this.blockedIPs.add(ip);
console.log(`IP ${ip} blocked due to repeated ${type} violations`);
}
}
validateOriginSyntax(origin) {
try {
const url = new URL(origin);
// HTTPSの強制
if (url.protocol !== 'https:') {
throw new Error('Only HTTPS origins are allowed');
}
// 不正な文字の検出
if (!/^[a-zA-Z0-9.-]+$/.test(url.hostname)) {
throw new Error('Invalid characters in origin hostname');
}
} catch (error) {
throw new Error(`Invalid origin format: ${error.message}`);
}
}
getClientIP(request) {
return request.headers['x-forwarded-for'] ||
request.headers['x-real-ip'] ||
request.connection.remoteAddress ||
request.socket.remoteAddress ||
'127.0.0.1';
}
}
WebSocketの持続接続特性を悪用したDDoS攻撃への対策が重要です。
class WebSocketRateLimiter {
constructor(options = {}) {
this.maxConnections = options.maxConnections || 1000;
this.maxConnectionsPerIP = options.maxConnectionsPerIP || 10;
this.maxMessagesPerSecond = options.maxMessagesPerSecond || 10;
this.maxMessageSize = options.maxMessageSize || 1024 * 16; // 16KB
this.connections = new Map(); // IP → 接続数
this.messageRates = new Map(); // 接続ID → メッセージレート情報
this.totalConnections = 0;
// 統計情報のクリーンアップ(1分ごと)
setInterval(() => this.cleanupStats(), 60000);
}
canAcceptConnection(request) {
const clientIP = this.getClientIP(request);
// 全体の接続数制限
if (this.totalConnections >= this.maxConnections) {
throw new Error('Server connection limit exceeded');
}
// IP別接続数制限
const ipConnections = this.connections.get(clientIP) || 0;
if (ipConnections >= this.maxConnectionsPerIP) {
throw new Error('IP connection limit exceeded');
}
return true;
}
registerConnection(ws, request) {
const clientIP = this.getClientIP(request);
const connectionId = this.generateConnectionId();
// 接続数更新
this.connections.set(clientIP, (this.connections.get(clientIP) || 0) + 1);
this.totalConnections++;
// メッセージレート制限の初期化
this.messageRates.set(connectionId, {
count: 0,
windowStart: Date.now(),
violations: 0
});
ws.connectionId = connectionId;
ws.clientIP = clientIP;
ws.on('close', () => {
this.unregisterConnection(ws);
});
return connectionId;
}
canAcceptMessage(ws, messageSize) {
// メッセージサイズ制限
if (messageSize > this.maxMessageSize) {
throw new Error('Message size limit exceeded');
}
const rateInfo = this.messageRates.get(ws.connectionId);
if (!rateInfo) {
throw new Error('Connection not registered');
}
const now = Date.now();
const windowDuration = 1000; // 1秒
// 新しいウィンドウの開始
if (now - rateInfo.windowStart >= windowDuration) {
rateInfo.count = 0;
rateInfo.windowStart = now;
}
// レート制限チェック
if (rateInfo.count >= this.maxMessagesPerSecond) {
rateInfo.violations++;
// 連続違反時は接続を切断
if (rateInfo.violations >= 3) {
throw new Error('Repeated rate limit violations');
}
throw new Error('Message rate limit exceeded');
}
rateInfo.count++;
return true;
}
}
企業環境でのWebSocket運用では、包括的なセキュリティポリシーが必要です。
# WebSocketセキュリティポリシー v1.0
## 1. 適用範囲
- 本ポリシーは、当社が運用するすべてのWebSocketサービスに適用される
- 開発、ステージング、本番環境すべてを対象とする
- サードパーティサービスとの統合時も準拠する
## 2. 暗号化要件
### 2.1 必須要件
- 本番環境では必ずwss://(TLS暗号化)を使用
- TLS 1.2以上を使用(TLS 1.3推奨)
- Perfect Forward Secrecy (PFS)対応暗号スイートを使用
### 2.2 証明書管理
- 証明書は信頼できるCA(Certificate Authority)から取得
- 証明書の有効期限は90日前に更新アラート
- 自動更新システムの導入を推奨
## 3. 認証・認可
### 3.1 認証要件
- すべてのWebSocket接続で認証を実装
- JWT、OAuth 2.0、またはSAML認証を使用
- 認証トークンの有効期限は最大24時間
### 3.2 認可制御
- ロールベースアクセス制御(RBAC)を実装
- 最小権限の原則を適用
- 管理者権限の分離
## 4. 入力検証
### 4.1 検証要件
- すべての入力データを検証・サニタイゼーション
- SQLインジェクション対策を実装
- XSS攻撃対策を実装
### 4.2 制限値
- メッセージサイズ上限: 16KB
- 接続あたりメッセージレート: 10msg/秒
- IP当たり同時接続数: 5接続
## 5. 監視・ログ
### 5.1 ログ要件
- すべての接続・切断をログ
- セキュリティ関連イベントの詳細ログ
- ログの改ざん防止措置
### 5.2 監視項目
- 異常な接続パターンの検知
- レート制限違反の監視
- 認証失敗の監視
## 6. インシデント対応
### 6.1 対応手順
- セキュリティインシデント発生時の連絡体制
- 緊急時の接続遮断手順
- 事後調査・改善手順
## 7. コンプライアンス
### 7.1 規制要件
- GDPR、CCPA等のデータ保護規制への準拠
- SOC 2、ISO 27001等のセキュリティ基準への準拠
- 業界固有の規制要件への対応
企業環境でのWebSocket実装時に確認すべきセキュリティ項目の包括的なチェックリストです。
認証・認可
暗号化・通信セキュリティ
入力検証・データ保護
レート制限・DDoS対策
監視・ログ・監査
規制対応
開発・運用プロセス
技術実装能力
運用・管理能力
コンプライアンス対応
このレッスンの内容を理解できましたら、完了マークをつけて次のレッスンに進みましょう。