なぜデータベースセキュリティが重要か
データベースは企業の最も重要な資産である「データ」を格納する場所です。顧客情報、取引履歴、機密文書など、価値の高いデータが集中しているため、攻撃者にとって魅力的なターゲットとなります。2023年のIBMのレポートによると、データ侵害の平均コストは445万ドルに上ります。適切なセキュリティ対策を怠ることは、財務的損失だけでなく、企業の信頼性にも深刻なダメージを与えます。
原則1:最小権限の原則(Principle of Least Privilege)
各アプリケーションやユーザーには、業務に必要な最小限の権限のみを付与します。例えば、レポート生成用のアカウントにはSELECT権限のみ、書き込みが必要なアプリには特定のテーブルへのINSERT/UPDATE権限のみを与えます。
-- レポート専用ユーザーの作成(読み取りのみ)
CREATE USER report_user WITH PASSWORD 'strong_password_here';
GRANT CONNECT ON DATABASE myapp TO report_user;
GRANT USAGE ON SCHEMA public TO report_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO report_user;
-- アプリケーション用ユーザー(必要なテーブルのみ)
CREATE USER app_user WITH PASSWORD 'another_strong_password';
GRANT SELECT, INSERT, UPDATE ON users, orders, products TO app_user;
REVOKE DELETE ON users FROM app_user; -- 削除は管理者のみ
原則2:SQLインジェクション対策
SQLインジェクションは最も古典的かつ危険な攻撃手法です。プリペアドステートメントとパラメータ化クエリを使用することで、ほぼ完全に防ぐことができます。
-- 危険なパターン(絶対に使用しないこと)
-- SELECT * FROM users WHERE username = '${username}'
-- 攻撃例:username = "' OR '1'='1"
-- 安全なパターン:プリペアドステートメント
PREPARE get_user(text) AS
SELECT id, username, email FROM users WHERE username = $1;
EXECUTE get_user('john_doe');
-- Node.js + pg の例
-- const result = await pool.query(
-- 'SELECT * FROM users WHERE id = $1',
-- [userId] -- パラメータは別途渡す
-- );
原則3:保存データの暗号化
パスワードは必ずハッシュ化して保存します。bcryptやArgon2などの専用アルゴリズムを使用し、MD5やSHA-1のような古いアルゴリズムは使用しません。機密性の高いカラム(クレジットカード番号、個人識別番号など)はカラムレベルの暗号化を検討します。
-- PostgreSQLのpgcryptoを使用した暗号化
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- 機密データの暗号化保存
INSERT INTO payment_methods (user_id, card_number_encrypted)
VALUES (
$1,
pgp_sym_encrypt($2, current_setting('app.encryption_key'))
);
-- 復号化
SELECT pgp_sym_decrypt(card_number_encrypted, current_setting('app.encryption_key'))
FROM payment_methods WHERE user_id = $1;
原則4:監査ログの実装
誰がいつ何を変更したかを追跡できる監査ログは、セキュリティインシデントの調査と規制準拠に不可欠です。
-- 監査ログテーブル
CREATE TABLE audit_logs (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
table_name VARCHAR(100) NOT NULL,
operation VARCHAR(10) NOT NULL, -- INSERT/UPDATE/DELETE
old_data JSONB,
new_data JSONB,
user_id UUID,
ip_address INET,
created_at TIMESTAMP DEFAULT NOW()
);
-- トリガーで自動記録
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs(table_name, operation, old_data, new_data, user_id)
VALUES (
TG_TABLE_NAME,
TG_OP,
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP != 'DELETE' THEN row_to_json(NEW) ELSE NULL END,
current_setting('app.current_user_id', true)::UUID
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;