I. Kiến thức chuẩn bị (tiếp)
5. Ưu, nhược điểm của JWT và lưu ý khi sử dụng
Ưu điểm
- Dữ liệu được mã hóa: JWT mã hóa thông tin người dùng bằng các thuật toán, đảm bảo tính bảo mật của dữ liệu khi truyền tải.
- Tính tích hợp: JWT có thể dễ dàng tích hợp vào nhiều ngôn ngữ lập trình khác nhau, thích nghi cao với phần lớn các công nghệ và ứng dụng hiện nay.
- Dễ dàng phát hiện khi dữ liệu bị thay đổi: JWT có chứa thông tin về chữ ký số (signature), giúp người nhận có thể kiểm tra tính toàn vẹn của dữ liệu.
- Cấu trúc linh hoạt: JWT cho phép thêm các thuộc tính mới vào payload một cách dễ dàng, giúp cho việc truyền tải thông tin giữa các ứng dụng trở nên linh hoạt hơn.
Nhược điểm
- Dữ liệu không được mã hóa đối với thuộc tính header và payload: Dữ liệu được truyền tải trong phần header và payload không được thực hiện mã hóa, giúp kẻ tấn công có thể dễ dàng đọc được nội dung.
- Không thể thu hồi token: Khi một token đã được tạo ra và sử dụng cho người dùng sẽ không thể thu hồi lại. Điều này đôi khi gây ra nguy cơ bảo mật trong trường hợp token bị đánh cắp hoặc bị lộ.
- Kích thước token lớn: Kích thước của JWT lớn hơn so với các phương pháp khác như session, cookie, gây tiêu tốn tài nguyên mạng và làm giảm tốc độ truyền dữ liệu.
6. Thực hành tạo ra một JWT cho chính mình
Chúng ta có thể tự tạo một JWT token bằng cách sử dụng thư viện JWT trong các ngôn ngữ lập trình, hoặc thủ công mã hóa đầu vào và tạo chữ ký số với thuật toán mã hóa đối xứng / không đối xứng.
Với thư viện JWT trong ngôn ngữ lập trình, việc tạo JWT token có thể được thực hiện đơn giản bằng cách cung cấp các thông tin cho header và payload, cùng với signature được tạo bằng khóa bí mật. Ví dụ, đây là cách tạo JWT token bằng thư viện PyJWT
trong Python:
import jwt payload = {'user_id': 123456}
# lưu ý nên sử dụng secret khó đoán
secret_key = 'my-secret-key'
algorithm = 'HS256' jwt_token = jwt.encode(payload, secret_key, algorithm)
print(jwt_token)
Một cách khác là thủ công tạo JWT token, các bạn có thể sử dụng một công cụ để mã hóa đầu vào và tạo chữ ký số với thuật toán mã hóa đối xứng hoặc không đối xứng. Ví dụ, đây là cách tạo JWT token bằng OpenSSL
và RSA
:
# Tạo khóa riêng tư RSA
openssl genpkey -algorithm RSA -out private_key.pem -aes256 # Tạo khóa công khai RSA từ khóa riêng tư
openssl rsa -pubout -in private_key.pem -out public_key.pem # Mã hóa dữ liệu và tạo chữ ký số sử dụng khóa riêng tư
echo '{"user_id": 123456}' | openssl dgst -sha256 -sign private_key.pem | base64 | tr -d '\n' # Tạo JWT token bằng payload và chữ ký số đã tạo
echo '{"alg": "RS256", "typ": "JWT"}' | base64 | tr -d '\n'; echo '.'; echo '{"user_id": 123456}' | base64 | tr -d '\n'; echo '.'; echo "<chữ ký số đã tạo>"
II. Tấn công JWT
Ở phần trước chúng ta đã tìm hiểu các kiến thức liên quan đến Json Web token (JWT). Bên cạnh tính tiện lợi, chúng ta cũng cần chú ý tới vấn đề an toàn khi sử dụng JWT trong ứng dụng. Chúng ta sẽ gọi chung là tấn công JWT.
Tấn công JWT là các phương pháp tấn công cố gắng tìm cách xâm nhập, giả mạo, đánh cắp hoặc giải mã các JWT không được cho phép. Dưới đây là một số phương pháp tấn công JWT phổ biến:
- JWT Signature Spoofing: Đây là phương pháp tấn công phổ biến nhất. Kẻ tấn công sẽ thay đổi chữ ký của JWT và tạo ra một JWT mới. Khi server xác thực JWT này, nó sẽ tin rằng JWT này được tạo ra bởi người dùng hợp lệ.
- JWT Tampering: Thay đổi các thông tin trong JWT, chẳng hạn như giá trị của các trường
iss
,sub
,aud
,... để tạo ra một JWT mới. Khi server xác thực JWT này, nó sẽ tin rằng JWT này được tạo ra bởi người dùng hợp lệ. - JWT Brute-force attacks: Cố gắng tìm ra secret key sử dụng trong phần signature bằng cách thử tất cả các khả năng có thể. Từ đó có thể sửa đổi nội dung thông tin truyền đạt nhằm giả mạo người dùng.
- JWT Encryption Attack: Giả mạo một JWT mới bằng cách mã hóa thông tin từ một JWT khác đã được xác thực. Khi server giải mã JWT mới, nó sẽ tin rằng JWT này được tạo ra bởi người dùng hợp lệ.
- JWT Timing Attack: Sử dụng thời gian phản hồi từ server để suy ra các thông tin trong JWT. Kẻ tấn công có thể sử dụng kỹ thuật này để suy ra chữ ký hoặc mã hóa của JWT và tạo ra một JWT mới.
Tất cả các phương pháp tấn công JWT chủ yếu đều nhằm mục đích giả mạo JWT và đánh lừa server để tin rằng JWT này được tạo ra bởi người dùng hợp lệ.
III. Phân tích các phương pháp tấn công JWT và biện pháp ngăn chặn
1. Thay đổi giá trị thông tin JWT
Một quy trình bình thường khi phía server tiếp nhận một request chứa JWT token có thể tóm tắt qua các bước sau:
- Tiếp nhận JWT từ người dùng và xác thực nó bằng cách kiểm tra chữ ký và xác thực token.
- Nếu JWT hợp lệ, thực hiện giải mã JWT để lấy thông tin trong phần payload của token.
- Tiếp tục các bước từ thông tin trên.
Chẳng hạn thư viện jsonwebtoken
của Node.js chứa các hàm verify()
và decode()
thực hiện chức năng xác thực và giải mã thông tin. Tuy nhiên, nếu trong quá trình xây dựng ứng dụng, lập trình viên trực tiếp sử dụng hàm decode()
mà thiếu đi bước kiểm tra xác thực, thì chương trình vẫn trả về các thông tin được lưu trữ trong payload của JWT, kể cả JWT token đã hết hạn!
Ví dụ đoạn chương trình xử lý JWT từ người dùng như sau:
const jwt = require('jsonwebtoken');
const secret = 'secret_key'; // Tiếp nhận JWT từ người dùng
const token = req.headers.authorization.split(' ')[1];
// Giải mã JWT và kiểm tra giá trị "admin"
const isAdmin = decoded.admin;
if (isAdmin) { // Nếu người dùng là admin, chuyển hướng tới trang dành cho quản trị viên res.redirect('/admin');
} else { // Nếu người dùng không phải là admin, chuyển hướng tới trang khác res.redirect('/user');
}
Do không chứa quá trình kiểm tra tính hợp lệ của JWT token, nên kẻ tấn công có thể tùy ý thay đổi thông tin JWT nhằm đánh lừa server dù không biết giá trị của secret key. Chẳng hạn, sau khi decode phần payload:
{ "username": "viblo-user", "isAdmin": false
}
Kẻ tấn công có thể thay đổi trường isAdmin
thành true
nhằm đánh lừa server với mục đích nâng quyền tài khoản người dùng thường bất kỳ.
Bài lab JWT authentication bypass via unverified signature minh họa cho tình huống này, bạn đọc có thể thay đổi giá trị JWT để mạo danh tài khoản administrator.
Từ đó, chúng ta cần lưu ý rằng quá trình xác thực JWT token là điều bắt buộc trong ứng dụng. Chương trình được thêm bước kiểm tra token:
const jwt = require('jsonwebtoken');
const secret = 'secret_key'; // Tiếp nhận JWT từ người dùng và xác thực nó
const token = req.headers.authorization.split(' ')[1];
jwt.verify(token, secret, (err, decoded) => { if (err) { // Xử lý lỗi xác thực JWT } else { // Giải mã JWT ... }
});