Đi tới nội dung chính
Cyber Security

OWASP Top 10 - Bài 2: A04 - Cryptographic Failures (Thủng Lưới Mật Mã)

5 tháng 7, 202611 phút2310 từZeno

Chào mọi người, Zeno quay trở lại rồi đây nghen! ☕✨

Sau bài mở bát về Injection, hôm nay tụi mình sẽ tiếp tục hành trình “soi” bảng phong thần với cái tên nằm ở vị trí thứ 4 trong bảng xếp hạng OWASP Top 10: A04:2025 - Cryptographic Failures (Lỗi Mật mã & Bảo mật Dữ liệu).

Nói tới “Cryptographic Failures” hay “Mật mã học”, nhiều anh em hay bị ngợp vì tưởng phải giải ma trận toán học hay bẻ khóa dữ thần như trong phim Hollywood. Nhưng sự thật nghiệt ngã ở thực tế là: Hacker hiếm khi bẻ gãy được các thuật toán mã hóa hiện đại, mà phần lớn hệ thống “bay màu” là do anh em dev… xài sai cách! :v

Giống như việc bạn mua một chiếc ổ khóa bằng thép nguyên khối siêu cứng chống cắt, nhưng lại lấy dây chun buộc nó vào cửa, hoặc tiện tay giấu luôn chìa khóa dưới thảm lau chân vậy đó. Chèng ơi!

Thôi, cùng Zeno xắn tay áo lên làm tới tới, bóc tách xem lỗi này sinh ra từ đâu và vá bằng Golang thế nào cho chuẩn bài nghen!


1. Bản chất của Cryptographic Failures là gì?

Trước đây trong bảng OWASP Top 10 (2017), lỗ hổng này mang tên là Sensitive Data Exposure (Rò rỉ dữ liệu nhạy cảm). Tuy nhiên, từ phiên bản 2021 đến 2025, OWASP đã đổi tên thành Cryptographic Failures để chỉ đích danh nguyên nhân gốc rễ: Sự thất bại hoặc sai sót trong quá trình áp dụng mật mã học (Cryptography) để bảo vệ dữ liệu.

Định nghĩa chuẩn bài: Cryptographic Failures xảy ra khi hệ thống thiếu mã hóa, mã hóa bằng thuật toán yếu/lỗi thời, hoặc quản lý chìa khóa mật mã kém cẩn thận, khiến dữ liệu nhạy cảm bị lộ lọt hoặc bị can thiệp.

Các mã lỗi CWE liên quan phổ biến theo tiêu chuẩn OWASP:

  • CWE-327: Sử dụng thuật toán mã hóa bị hỏng hoặc có rủi ro cao (Broken or Risky Cryptographic Algorithm).
  • CWE-331: Độ ngẫu nhiên/Hỗn loạn không đủ lớn (Insufficient Entropy).
  • CWE-338: Sử dụng bộ sinh số giả ngẫu nhiên yếu về mặt mật mã (Weak PRNG).
  • CWE-326: Độ dài khóa mã hóa không đủ mạnh (Inadequate Encryption Strength).

Dữ liệu nhạy cảm cần bảo vệ bao gồm:

  • Mật khẩu người dùng, token phiên làm việc (Session Tokens, JWT).
  • Thông tin cá nhân (PII): Số CCCD, số điện thoại, địa chỉ, hồ sơ y tế.
  • Thông tin tài chính: Số thẻ tín dụng (PAN), CVV, tài khoản ngân hàng.
  • Bí mật hệ thống: Secret Keys, API Keys, Database Passwords.

Nếu các dữ liệu này không được bảo vệ đúng cách cả khi truyền đi trên mạng (In-Transit) lẫn khi lưu trữ trong Database (At-Rest), hệ thống của bạn coi như “mở cửa đón trộm” rồi nghen!


2. Phân biệt nhập môn: Hashing (Băm) vs Encryption (Mã hóa)

Trước khi đi sâu vào bug, Zeno phải “clear” ngay cái khái niệm này vì rất nhiều bạn vẫn hồn nhiên bảo: “Dự án em mã hóa mật khẩu bằng SHA256” :D

Hãy nhớ thần chú này của Zeno nha:

  • Mã hóa (Encryption) là đường 2 chiều: Bạn khóa lại được thì phải mở ra được (cần có Khóa/Key).
    • Mục đích: Giấu dữ liệu để truyền qua mạng hoặc lưu trữ, nhưng sau này hệ thống vẫn cần giải mã ra để đọc lại (Ví dụ: Số thẻ tín dụng, CCCD, Email).
    • Thuật toán phổ biến: AES-256-GCM (Đối xứng - Symmetric), RSA / ECC (Bất đối xứng - Asymmetric).
  • Băm dữ liệu (Hashing) là đường 1 chiều (One-Way Function): Đã băm thành chả rồi thì không cách nào ráp lại thành con lợn được!
    • Mục đích: Kiểm tra tính toàn vẹn (Integrity) hoặc lưu mật khẩu. Khi người dùng đăng nhập, ta băm pass nhập vào rồi so sánh với chuỗi băm trong DB.
    • Thuật toán băm mật khẩu chuẩn (Key Stretching): Argon2id, Bcrypt, PBKDF2.
Tiêu chíHashing (Băm dữ liệu)Encryption (Mã hóa)
Tính thuận nghịch1 chiều (Không giải mã ngược lại được)2 chiều (Giải mã được nếu có Khóa đúng)
Độ dài kết quảCố định (Dù băm 1 chữ hay 1 cuốn sách)Biến đổi theo độ dài dữ liệu gốc
Yếu tố đi kèmCần thêm Salt (chuỗi ngẫu nhiên chống Rainbow Table)Cần Key (Khóa) & IV/Nonce
Ứng dụng chínhLưu mật khẩu, verify checksum fileMã hóa kết nối HTTPS, lưu số thẻ tín dụng

3. 4 “Bẫy Tử Thần” gây ra Cryptographic Failures

Lỗi mã hóa thường đến từ 4 kịch bản “sập hầm” kinh điển sau:

3.1. Dữ liệu truyền đi không mã hóa (In-Transit)

Dùng HTTP thay vì HTTPS, hoặc SMTP/FTP truyền rõ Plaintext. Hacker đứng cùng mạng Wifi công cộng (như ở quán cà phê) xài Wireshark bắt gói tin một cái là thấy sạch sẽ Username/Password luôn. Mèng ơi!

3.2. Dùng thuật toán lỗi thời hoặc dùng sai mục đích

  • Dùng MD5 hay SHA-1 để băm mật khẩu: Mấy cái này giờ các trang giải mã online giải trong 0.001 giây bằng Rainbow Table.
  • Dùng SHA-256 để lưu mật khẩu: SHA-256 là Cryptographic Hash Function được thiết kế để tính toán cực nhanh. Hacker dùng Card màn hình (GPU) có thể thử hàng tỷ pass/giây để Brute-force. Mật khẩu phải xài thuật toán băm chậm cố ý (Password Hashing Algorithm) như Bcrypt hay Argon2id!

3.3. Khóa mật mã (Key Management) kém an toàn

Khóa rất an toàn nhưng lại hardcode thẳng vào mã nguồn: const secretKey = "my-super-secret-123". Xong vô tình bấm git push lên GitHub công khai. Thế là xong phim!

3.4. Dùng bộ sinh số ngẫu nhiên yếu (Weak PRNG)

Lấy hàm Math.random() (JS) hay math/rand (Go) để tạo Token reset mật khẩu hoặc JWT secret. Mấy hàm này là Pseudo-Random Number Generator (PRNG), tạo số ngẫu nhiên theo công thức toán học nội tại, hacker hoàn toàn đoán được chuỗi ngẫu nhiên tiếp theo!


4. Thực hành Chống Lỗi Mật Mã bằng Golang!

Golang hỗ trợ gói crypto trong thư viện chuẩn rất ngon lành, nhưng nếu dùng nhầm API thì vẫn “ăn hành” như thường nghen.

4.1. Case 1: Mã hóa dữ liệu At-Rest (AES-ECB vs AES-GCM)

[Cảnh báo] Code lỗi “toang nặng” (CWE-327): Lỗi này vừa hardcode Key, vừa tự viết vòng lặp mã hóa dạng ECB (Electronic Codebook). Chế độ ECB mã hóa từng khối 16-byte độc lập mà không có IV/Nonce. Các khối dữ liệu giống nhau sẽ ra ciphertext giống hệt nhau, làm lộ cấu trúc dữ liệu!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// LỖI 1: Hardcode Secret Key trực tiếp trong code
const secretKey = "my-secret-key-123" 

func UnsafeEncryptECB(plaintext []byte) []byte {
	block, _ := aes.NewCipher([]byte(secretKey[:16]))
	ciphertext := make([]byte, len(plaintext))
	
	// LỖI 2: Mã hóa kiểu ECB (vòng lặp từng block độc lập không có Nonce/IV)
	for i := 0; i < len(plaintext); i += block.BlockSize() {
		block.Encrypt(ciphertext[i:i+block.BlockSize()], plaintext[i:i+block.BlockSize()])
	}
	return ciphertext
}

[Khuyến nghị] Code chuẩn bài với AES-256-GCM (AEAD): Dùng chế độ AES-GCM (Galois/Counter Mode) - một thuật toán AEAD (Authenticated Encryption with Associated Data). Nó vừa mã hóa bảo mật vừa tạo Authentication Tag chống sửa đổi dữ liệu. Key đọc từ biến môi trường (os.Getenv).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"errors"
	"io"
	"os"
)

func SafeEncryptGCM(plaintext []byte) ([]byte, error) {
	// 1. Đọc Key từ biến môi trường của OS (Không hardcode! Yêu cầu 32 bytes cho AES-256)
	key := []byte(os.Getenv("APP_ENCRYPTION_KEY"))
	if len(key) != 32 {
		return nil, errors.New("khóa mã hóa phải đúng 32 bytes nghen anh em")
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	// 2. Sử dụng chế độ GCM (AEAD) cực kỳ an toàn
	aesGCM, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	// 3. Tạo chuỗi Nonce (12 bytes) ngẫu nhiên bằng CSPRNG ngẫu nhiên 100% cho mỗi lần mã hóa
	nonce := make([]byte, aesGCM.NonceSize())
	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}

	// 4. Mã hóa và nối Nonce vào đầu Ciphertext để tiện phục vụ giải mã sau này
	return aesGCM.Seal(nonce, nonce, plaintext, nil), nil
}

4.2. Case 2: Sinh Token bảo mật (math/rand vs crypto/rand)

Anh em hay có thói quen tạo Token khôi phục mật khẩu bằng math/rand. Cái này là “biếu” tài khoản cho hacker luôn đó!

[Cảnh báo] Code dùng PRNG yếu (CWE-338):

1
2
3
4
5
6
7
8
// LỖI: math/rand KHÔNG an toàn cho các tác vụ mật mã/bảo mật!
func UnsafeGenerateToken() string {
	// Seed theo Nano giây vẫn có thể bị tính toán để đoán ngược trạng thái
	rand.Seed(time.Now().UnixNano()) 
	b := make([]byte, 16)
	rand.Read(b)
	return fmt.Sprintf("%x", b)
}

[Khuyến nghị] Code chuẩn 100 điểm với CSPRNG: Sử dụng gói crypto/rand. Gói này gọi trực tiếp bộ sinh số ngẫu nhiên an toàn mật mã của Hệ điều hành (Cryptographically Secure PRNG - CSPRNG), hacker bó tay toàn tập!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import (
	"crypto/rand"
	"fmt"
)

// GIẢI PHÁP: Dùng crypto/rand cho mọi Token, Session ID, OTP
func SafeGenerateToken() (string, error) {
	b := make([]byte, 16)
	// Đọc ngẫu nhiên an toàn từ OS (CSPRNG)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return fmt.Sprintf("%x", b), nil
}

4.3. Case 3: Băm mật khẩu chuẩn bài bằng Bcrypt

Tuyệt đối không băm pass bằng SHA256 hay MD5 nha! Trong Go, chuẩn nhất là dùng thư viện golang.org/x/crypto/bcrypt (nó tự động nêm thêm Salt (muối) ngẫu nhiên vào mật khẩu — thấy chưa, đôi khi cuộc sống hay code mặn mà một chút lại tốt hơn hẳn! :v).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import (
	"golang.org/x/crypto/bcrypt"
)

// Băm mật khẩu (Tự động nêm Salt ngẫu nhiên và làm chậm tiến trình băm)
func HashPassword(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	return string(bytes), err
}

// Kiểm tra mật khẩu khi đăng nhập
func CheckPasswordHash(password, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil
}

4.4. Case 4: Cấu hình Server ép TLS 1.3 cứng cựa

Đừng để server Golang của bạn chấp nhận kết nối từ các giao thức cũ kỹ dễ dính lỗi như TLS 1.0 hay 1.1 nghen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import (
	"crypto/tls"
	"net/http"
	"time"
)

func StartSecureServer() error {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, HTTPS an toàn tuyệt đối!"))
	})

	// Chỉ cho phép kết nối TLS 1.3 trở lên để đảm bảo Perfect Forward Secrecy (PFS)
	tlsConfig := &tls.Config{
		MinVersion:               tls.VersionTLS13,
		PreferServerCipherSuites: true,
	}

	server := &http.Server{
		Addr:         ":443",
		Handler:      mux,
		TLSConfig:    tlsConfig,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	return server.ListenAndServeTLS("server.crt", "server.key")
}

5. Cạm Bẫy Bảo Mật JWT trong Hệ Thống Backend

JWT (JSON Web Token) sử dụng mật mã học để ký số và xác thực, nhưng nếu cấu hình sai thì rất dễ dính chấu!

5.1. Lỗi thuật toán ký số: HS256 vs RS256

  • HS256 (HMAC-SHA256 Đối xứng): Dùng cùng 1 Secret Key để cả Auth Service ký token và các Service khác verify token. Nếu một microservice bất kỳ lộ Secret Key, hacker có thể tự tạo ra JWT hợp lệ với quyền Admin luôn!
  • RS256 (RSA Signature Bất đối xứng): Auth Service dùng Private Key để ký, còn các service phụ chỉ cần giữ Public Key để kiểm tra signature. Dù service phụ bị hack cũng không ai lấy được Private Key để giả mạo token.

5.2. Lỗi đổi thuật toán “alg”: “none” (CWE-347)

Nhiều thư viện JWT cũ ngây thơ chấp nhận token có header {"alg": "none"} (không dùng chữ ký). Hacker chỉ cần sửa user_id thành Admin rồi bỏ phần chữ ký đi là nghiễm nhiên vượt qua khâu đăng nhập!

Lời khuyên của Zeno: Luôn chỉ định rõ thuật toán cho phép (Whitelist) khi parse JWT trong Go:

1
2
3
4
5
6
7
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
    // Ép bắt buộc phải là RS256!
    if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
        return nil, fmt.Errorf("thuật toán không hợp lệ nha: %v", token.Header["alg"])
    }
    return verifyKey, nil
})

6. Tự Động Hóa Quét Secret

Làm dev thì ai cũng có lúc bất cẩn. Cách tốt nhất để không bị rò rỉ Key là ép máy móc tự gác cổng!

Anh em nên cài đặt công cụ như gitleaks hoặc trufflehog vào pre-commit hook hoặc CI/CD pipeline để nó tự động chặn nếu lỡ tay commit file .env hay AWS Key nhé:

1
2
# Quét repository xem có lỡ dính lộ Key nào không
gitleaks detect --source . -v

Tóm lại

Bảo mật mật mã nghe thì cao siêu nhưng nguyên tắc vàng đầu tiên của dân IT chính là: “ĐỪNG TỰ PHÁT MINH LẠI BÁNH XE” (Don’t roll your own crypto)!

  • Luôn dùng thư viện chuẩn (crypto/rand, bcrypt, AES-GCM).
  • Giấu Secret Key vào Vault / Env Variables.
  • Ép HTTPS & TLS 1.3 trên mọi kết nối.

Chúc anh em code an toàn, không bao giờ bị “thủng lưới mật mã” nghen! Hẹn gặp lại ở các bài OWASP tiếp theo! :D


Tài liệu tham khảo

Chủ đề của bài viết

Chia sẻ bài viết

Gửi bài này cho người đang cần đúng chủ đề, hoặc lưu lại để quay về sau.

Đọc tiếp

Tất cả bài viết

Tiếp theo

Gợi ý bài tiếp theo

OWASP Top 10 - Bài 1: A05 - Injection (Lỗ hổng Tiêm nhiễm)

Chào mọi người, Zeno đây! Hôm nay mình sẽ chính thức khởi động series OWASP Top 10 nha. Dành cho những ai chưa biết thì đây giống như là bảng phong thần xếp …

Mở bài tiếp theo

Thảo luận