Trước đây được gọi là "Lỗi lộ dữ liệu nhạy cảm", Lỗi mật mã tập trung vào các lỗi liên quan đến mật mã dẫn đến lộ dữ liệu nhạy cảm. Lỗ hổng này nằm ở vị trí thứ 2 trong Top 10 của OWASP vì mật mã không đúng cách đã dẫn đến một số vụ vi phạm dữ liệu nghiêm trọng nhất trong lịch sử.
Lỗi mật mã là gì?
Lỗi mật mã xảy ra khi ứng dụng:
- Truyền dữ liệu nhạy cảm ở dạng văn bản rõ ràng (HTTP, FTP, SMTP không có TLS)
- Sử dụng thuật toán mã hóa yếu hoặc lỗi thời (MD5, SHA1, DES)
- Lưu trữ mật khẩu không chính xác (văn bản thuần túy, băm đơn giản, muối yếu)
- Sử dụng khóa mã hóa cứng hoặc yếu
- Không thể thực thi mã hóa (nội dung hỗn hợp, TLS tùy chọn)
Hậu quả bao gồm từ thông tin bị đánh cắp đến việc lộ toàn bộ cơ sở dữ liệu. Khi mật mã không thành công, thường sẽ không có cơ hội thứ hai—một khi dữ liệu bị lộ, nó không thể không bị lộ.
Các mẫu lỗ hổng phổ biến
1. Mật khẩu được lưu trữ không chính xác
Lưu trữ mật khẩu là nơi nhiều ứng dụng bị lỗi. Chúng ta hãy nhìn vào sự tiến triển từ tệ nhất đến tốt nhất:
Level 0 - Văn bản thuần túy (Không bao giờ làm điều này):
// CATASTROPHIC: Plain text password storage
const user = new User({
email: req.body.email,
password: req.body.password // Stored as-is!
});
Nếu cơ sở dữ liệu của bạn bị xâm phạm, mọi mật khẩu sẽ bị xâm phạm ngay lập tức. Người dùng thường sử dụng lại mật khẩu nên hành vi vi phạm này còn mở rộng sang các tài khoản khác của họ.
Cấp 1 - Băm đơn giản (Vẫn nguy hiểm):
// DANGEROUS: Simple hash without salt
const crypto = require('crypto');
const hashedPassword = crypto.createHash('sha256')
.update(req.body.password)
.digest('hex');
Điều này dễ bị tấn công bởi bảng cầu vồng. Những kẻ tấn công đã tính toán trước hàm băm cho hàng triệu mật khẩu phổ biến. Việc tra cứu mất một phần nghìn giây.
Level 2 - Băm bằng muối (Tốt hơn nhưng chưa đủ):
// INSUFFICIENT: SHA-256 with salt
const salt = crypto.randomBytes(16).toString('hex');
const hashedPassword = crypto.createHash('sha256')
.update(salt + req.body.password)
.digest('hex');
Salting đánh bại bảng cầu vồng, nhưng SHA-256 quá nhanh. Các GPU hiện đại có thể tính toán hàng tỷ hàm băm SHA-256 mỗi giây, khiến các cuộc tấn công vũ phu trở nên khả thi.
Cấp 3 - Băm mật khẩu hàm (Phương pháp đúng):
// SECURE: Use bcrypt with appropriate cost factor
const bcrypt = require('bcrypt');
// Cost factor of 12 means 2^12 iterations
// Adjust based on your security needs and hardware
const SALT_ROUNDS = 12;
async function hashPassword(plainPassword) {
// bcrypt automatically generates a salt and includes it in the output
return await bcrypt.hash(plainPassword, SALT_ROUNDS);
}
async function verifyPassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
// Usage
const hash = await hashPassword('userPassword123');
// Result: $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYE7zNqK7Kxy
// Contains: algorithm version, cost factor, salt, and hash
Bcrypt, Argon2 và scrypt được thiết kế để hoạt động chậm và tốn nhiều bộ nhớ, khiến các cuộc tấn công vũ phu trở nên không thực tế. Argon2 là người chiến thắng trong Cuộc thi băm mật khẩu và được đề xuất cho các ứng dụng mới:
// RECOMMENDED: Argon2id for new applications
const argon2 = require('argon2');
async function hashPassword(plainPassword) {
return await argon2.hash(plainPassword, {
type: argon2.argon2id, // Hybrid of Argon2i and Argon2d
memoryCost: 65536, // 64 MB memory
timeCost: 3, // 3 iterations
parallelism: 4 // 4 parallel threads
});
}
async function verifyPassword(plainPassword, hashedPassword) {
return await argon2.verify(hashedPassword, plainPassword);
}
2. Dữ liệu nhạy cảm khi chuyển tuyến
Dữ liệu được truyền mà không mã hóa có thể bị chặn bởi bất kỳ ai trên đường dẫn mạng.
Mẫu dễ bị tổn thương:
// VULNERABLE: HTTP API calls
fetch('http://api.example.com/users', {
method: 'POST',
body: JSON.stringify({ email, password })
});
Triển khai an toàn:
// Server configuration - enforce HTTPS
const express = require('express');
const helmet = require('helmet');
const app = express();
// Helmet adds security headers including HSTS
app.use(helmet());
// Force HTTPS redirect
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.header('host')}${req.url}`);
}
next();
});
// Set secure cookie options
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
secure: true, // Only send over HTTPS
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict' // CSRF protection
}
}));
Định cấu hình HSTS (HTTP Strict Transport Security) để đảm bảo trình duyệt luôn sử dụng HTTPS:
# Nginx configuration
server {
listen 443 ssl http2;
# HSTS header - browser will refuse HTTP for 1 year
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Modern TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
}
3. Mã hóa yếu cho dữ liệu ở phần còn lại
Việc mã hóa dữ liệu nhạy cảm ở trạng thái lưu trữ sẽ bảo vệ khỏi vi phạm cơ sở dữ liệu, nhưng chỉ khi được thực hiện đúng cách.
Mẫu dễ bị tổn thương:
// VULNERABLE: ECB mode preserves patterns in data
const cipher = crypto.createCipher('aes-256-ecb', key);
// VULNERABLE: Hard-coded encryption key
const ENCRYPTION_KEY = 'my-super-secret-key-12345';
// VULNERABLE: Predictable IV (Initialization Vector)
const iv = Buffer.alloc(16, 0); // All zeros!
Triển khai an toàn:
const crypto = require('crypto');
// Key should come from environment/secrets manager
// Must be 32 bytes for AES-256
const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
function encrypt(plainText) {
// Generate a random IV for each encryption
// IV doesn't need to be secret, but must be unique
const iv = crypto.randomBytes(16);
// Use AES-256-GCM (authenticated encryption)
const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
let encrypted = cipher.update(plainText, 'utf8', 'hex');
encrypted += cipher.final('hex');
// GCM provides an authentication tag - store this too
const authTag = cipher.getAuthTag();
// Return IV + authTag + ciphertext (all needed for decryption)
return {
iv: iv.toString('hex'),
authTag: authTag.toString('hex'),
content: encrypted
};
}
function decrypt(encryptedData) {
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData.content, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
AES-GCM cung cấp mã hóa xác thực, nghĩa là nó vừa mã hóa dữ liệu vừa xác minh dữ liệu không bị giả mạo. Thẻ xác thực sẽ khiến việc giải mã không thành công nếu bản mã đã bị sửa đổi.
4. Quản lý khóa không đúng cách
Mã hóa mạnh nhất sẽ vô giá trị nếu khóa bị xử lý sai.
Những lỗi thường gặp:
// WRONG: Key in source code
const API_KEY = 'sk-1234567890abcdef';
// WRONG: Key in version control
// .env file committed to git
// WRONG: Same key for all environments
// Production using the same key as development
Quản lý khóa thích hợp:
// Use environment variables at minimum
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error('API_KEY environment variable is required');
}
// Better: Use a secrets manager
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
async function getSecret(secretName) {
const client = new SecretManagerServiceClient();
const [version] = await client.accessSecretVersion({
name: `projects/my-project/secrets/${secretName}/versions/latest`
});
return version.payload.data.toString();
}
// AWS Secrets Manager example
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
async function getAWSSecret(secretId) {
const data = await secretsManager.getSecretValue({ SecretId: secretId }).promise();
return JSON.parse(data.SecretString);
}
Biện pháp phòng ngừa tốt nhất
1. Phân loại dữ liệu của bạn
Không phải tất cả dữ liệu đều cần mức độ bảo vệ như nhau. Phân loại dữ liệu theo độ nhạy:
// Data classification example
const DataClassification = {
PUBLIC: 'public', // Marketing content, public docs
INTERNAL: 'internal', // Employee directory, internal docs
CONFIDENTIAL: 'confidential', // Customer data, financial records
RESTRICTED: 'restricted' // PII, payment data, health records
};
// Apply appropriate controls based on classification
function getEncryptionRequirements(classification) {
switch (classification) {
case DataClassification.RESTRICTED:
return {
atRest: true,
inTransit: true,
algorithm: 'aes-256-gcm',
keyRotation: '30d'
};
case DataClassification.CONFIDENTIAL:
return {
atRest: true,
inTransit: true,
algorithm: 'aes-256-gcm',
keyRotation: '90d'
};
default:
return {
atRest: false,
inTransit: true, // Always encrypt in transit
algorithm: null,
keyRotation: null
};
}
}
2. Sử dụng cấu hình TLS hiện đại
# Example: Kubernetes Ingress with modern TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-ssl-protocols: "TLSv1.2 TLSv1.3"
spec:
tls:
- hosts:
- api.example.com
secretName: api-tls-cert
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 443
3. Thực hiện xoay phím
Khóa mã hóa phải được luân chuyển thường xuyên. Đây là mẫu để hỗ trợ xoay vòng phím:
// Support multiple key versions for rotation
const keys = {
'v1': Buffer.from(process.env.ENCRYPTION_KEY_V1, 'hex'),
'v2': Buffer.from(process.env.ENCRYPTION_KEY_V2, 'hex'), // Current
};
const CURRENT_KEY_VERSION = 'v2';
function encrypt(plainText) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', keys[CURRENT_KEY_VERSION], iv);
let encrypted = cipher.update(plainText, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
keyVersion: CURRENT_KEY_VERSION, // Store which key was used
iv: iv.toString('hex'),
authTag: cipher.getAuthTag().toString('hex'),
content: encrypted
};
}
function decrypt(encryptedData) {
// Use the key version that was used for encryption
const key = keys[encryptedData.keyVersion];
if (!key) {
throw new Error(`Unknown key version: ${encryptedData.keyVersion}`);
}
const decipher = crypto.createDecipheriv('aes-256-gcm', key,
Buffer.from(encryptedData.iv, 'hex'));
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.content, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Re-encrypt data with new key during rotation
async function rotateEncryption(record) {
if (record.encryptedData.keyVersion !== CURRENT_KEY_VERSION) {
const plainText = decrypt(record.encryptedData);
record.encryptedData = encrypt(plainText);
await record.save();
}
}
Bài học chính
- Sử dụng các chức năng băm mật khẩu (bcrypt, Argon2) chứ không phải các hàm băm có mục đích chung
- EMã hóa dữ liệu khi truyền - HTTPS ở mọi nơi, không có ngoại lệ
- Sử dụng mã hóa xác thực (AES-GCM) cho dữ liệu ở trạng thái lưu trữ
- Không bao giờ khóa mã cứng - sử dụng các biến môi trường hoặc trình quản lý bí mật
- Tạo IVs duy nhất cho mỗi hoạt động mã hóa
- Plan xoay vòng khóa - bao gồm phiên bản khóa trong dữ liệu được mã hóa
- Phân loại dữ liệu của bạn - áp dụng các điều khiển thích hợp dựa trên độ nhạy
- Sử dụng TLS hiện đại - TLS 1.2+ với bộ mật mã mạnh
Các lỗi về mật mã thường không được phát hiện cho đến khi xảy ra vi phạm. Bằng cách làm theo những biện pháp này, bạn đảm bảo rằng ngay cả khi kẻ tấn công có được quyền truy cập vào bộ lưu trữ của bạn thì dữ liệu vẫn được bảo vệ.