🔑 Lộ API key trong repo và quản lý biến môi trường

By Better Wise

Bạn đã đi qua module 1 với SQL injection, XSS và CSRF, tức là những lỗi nằm trong cách code xử lý dữ liệu. Module này mở ra một nhóm rủi ro khác, thường chẳng liên quan gì đến logic nghiệp vụ: cấu hình và secrets. Đây là chỗ AI viết code ẩu nhiều nhất mà người review hay bỏ qua, vì đoạn code vẫn chạy trơn tru.

Sau bài này, bạn sẽ:

  • Chỉ ra được secret bị hardcode trong một đoạn code AI vừa sinh.
  • Chuyển cấu hình từ hardcode sang biến môi trường đúng cách.
  • Nhận biết cái bẫy biến môi trường ở frontend.
  • Biết thứ tự xử lý khi key đã lỡ bị commit.

Vì sao một dòng key lại thành sự cố

Chuyện thật, và lặp lại hàng tuần: một dev cần chạy nhanh, dán key của dịch vụ thanh toán vào file config.js, push lên repo public. Bot quét GitHub tìm các chuỗi giống API key không cần biết bạn là ai. Vài phút sau key đó được dùng ở nơi khác. Không có hacker nào nhắm vào bạn cả, chỉ có bot chạy 24/7.

Điều nhiều người hiểu sai: repo private không phải két sắt. Nó vẫn nằm trên máy nhiều người, trong bản fork, trong log CI, trong ảnh chụp màn hình. Và quan trọng nhất: xóa file không xóa nội dung khỏi lịch sử git.

AI hay hardcode secret ở đâu

AI sinh code dựa trên pattern nó từng thấy. Rất nhiều repo ngoài kia hardcode key, nên AI tái tạo lại đúng thói quen xấu đó, và nó không biết key của bạn là thật hay giả.

Ví dụ 1: file config "cho nhanh"

// config.js do AI sinh
const config = {
  apiKey: "sk_live_DAY_LA_DU_LIEU_MAU",
  dbUrl: "postgres://admin:matkhau123@localhost:5432/app"
};

Một chỗ chứa hai secret: API key và mật khẩu database nằm ngay trong connection string.

Ví dụ 2: gọi API bên thứ ba

fetch("https://api.payment.example/charge", {
  headers: { Authorization: "Bearer sk_live_DAY_LA_DU_LIEU_MAU" }
});

Ví dụ 3: những chỗ ít ai soi

Key nằm trong file seed dữ liệu, trong comment ở đầu file, trong notebook thử nghiệm, trong script migration. Bạn mở 5 file đầu thì không thấy, file thứ 6 thì có.

Dấu hiệu nhận biết khi đọc code AI: chuỗi dài, trông ngẫu nhiên, có tiền tố kiểu sk_, ghp_, AKIA; mật khẩu nằm trong URL kết nối; hoặc file .env xuất hiện trong danh sách file thay đổi.

Chuyển sang biến môi trường

Nguyên tắc duy nhất cần nhớ: code chỉ chứa tên biến, không chứa giá trị.

// đọc từ biến môi trường, không hardcode
const apiKey = process.env.PAYMENT_API_KEY;
if (!apiKey) {
  throw new Error("Thiếu biến môi trường PAYMENT_API_KEY");
}

File .env ở máy bạn (không commit):

PAYMENT_API_KEY=gia_tri_that_cua_ban
DATABASE_URL=postgres://...

.gitignore phải có:

.env
.env.local
*.pem

Và commit file .env.example chỉ gồm tên biến, không có giá trị thật:

PAYMENT_API_KEY=
DATABASE_URL=

Mỗi môi trường (local, staging, production) có bộ biến riêng, đặt trong phần cấu hình của nền tảng deploy. Đổi môi trường không cần sửa một dòng code nào.

Nhanh tay kiểm tra:

  • Secret phải được đọc từ _____ thay vì viết trực tiếp trong code.
  • File _____ chứa giá trị thật và phải nằm trong .gitignore; file _____ được commit và chỉ chứa tên biến.
  • Khi key đã lỡ bị commit, bước đầu tiên là _____.
  • Biến có tiền tố NEXT_PUBLIC_ hoặc VITE_ sẽ bị _____ nên tuyệt đối không dùng cho secret.

Cái bẫy: biến môi trường ở frontend không phải secret

Đây là lỗi AI mắc nhiều nhất trong các dự án React hay Next.js. Nghe "biến môi trường" là thấy an toàn, nhưng không phải.

// SAI: biến có tiền tố NEXT_PUBLIC_ bị nhúng thẳng vào bundle
const key = process.env.NEXT_PUBLIC_PAYMENT_SECRET_KEY;

Bất cứ thứ gì bắt đầu bằng NEXT_PUBLIC_ (Next.js) hay VITE_ (Vite) đều được build vào file JavaScript gửi xuống trình duyệt. Mở DevTools là thấy. Biến môi trường chỉ an toàn khi nó ở phía server.

Quy tắc: biến public chỉ dùng cho thứ vốn công khai (URL API, tên miền, mã hiển thị). Khóa bí mật phải được gọi từ server, client gọi vào server của bạn rồi server mới gọi dịch vụ ngoài.

Khi key đã lỡ bị commit

Thứ tự này quan trọng, đừng đảo lộn:

  1. Rotate ngay: vào dashboard nhà cung cấp, thu hồi key cũ và tạo key mới. Đây là bước duy nhất thực sự chặn thiệt hại.
  2. Kiểm tra log sử dụng của key cũ xem có lượt gọi lạ nào không, ghi lại thời điểm.
  3. Xóa secret khỏi lịch sử git bằng công cụ chuyên dụng (git filter-repo hoặc BFG), rồi thông báo cho team để mọi người pull lại.
  4. Thêm .env vào .gitignore, bật secret scanning trong CI để lần sau có cảnh báo trước khi merge.

Sai lầm phổ biến: xóa file, tạo commit mới, coi như xong. Key vẫn nằm nguyên trong commit cũ, và bất kỳ ai clone repo đều lấy được.

Mẹo soi secret trước khi push

  • Trước khi commit, tự hỏi: nếu chuỗi này bị dán lên một trang công khai, mình có thấy lo không? Nếu có, nó không được nằm trong code.
  • Đừng bao giờ "tạm commit" key, kể cả repo private, kể cả nhánh feature sẽ xóa sau. Nhánh cũng được đẩy lên remote.
  • Không gửi key qua chat, email hay ticket. Dùng secret manager của nền tảng hoặc trình quản lý mật khẩu của team.
  • AI thường quên kiểm tra biến môi trường có tồn tại không. Thiếu biến thì app chạy với undefined và lỗi hiện ra ở chỗ khác, rất khó truy.
  • Mỗi lần thêm biến mới, cập nhật .env.example cùng lúc. Không làm thì người deploy sau sẽ thiếu biến và đoán mò.
  • Secret scanning trong CI rẻ hơn nhiều so với một lần rotate khẩn cấp.

Ghi nhớ và bước tiếp theo

Bốn điều cần mang theo: secret bị hardcode là lỗi thật dù code vẫn chạy; code chỉ giữ tên biến, giá trị nằm ở môi trường; biến frontend thì ai cũng đọc được; và khi key lộ, rotate trước rồi mới dọn lịch sử.

Bài sau chúng ta đi tiếp phần còn lại của cấu hình: bật HTTPS, kiểm tra phân quyền cơ bản và validate input ở server, tức là những chỗ mà một đoạn code AI trông rất gọn nhưng lại mở cửa hậu.

Front: Vì sao hardcode API key trong code là nguy hiểm? Back: Key nằm trong repo và cả lịch sử git, ai đọc được repo đều lấy được.

Front: Vì sao AI hay sinh code hardcode secret? Back: Nó học từ nhiều repo từng hardcode key nên tái tạo lại đúng pattern đó.

Front: Dấu hiệu một chuỗi có thể là secret? Back: Chuỗi dài, trông ngẫu nhiên, có tiền tố như sk_, ghp_, AKIA, hoặc mật khẩu nằm trong connection string.

Front: Cách đúng để code dùng secret là gì? Back: Đọc từ biến môi trường, code chỉ chứa tên biến chứ không chứa giá trị.

Front: File nào tuyệt đối không được commit? Back: .env và mọi file chứa giá trị thật như .pem, .env.local.

Front: File nào nên commit để làm mẫu cấu hình? Back: .env.example, chỉ gồm tên biến và không có giá trị thật.

Front: Vì sao xóa file .env rồi commit lại vẫn chưa đủ? Back: Lịch sử git vẫn giữ nguyên nội dung cũ trong các commit trước.

Front: Bước đầu tiên khi phát hiện key đã bị commit? Back: Rotate ngay: thu hồi key cũ và tạo key mới trên dashboard nhà cung cấp.

Front: Tiền tố NEXT_PUBLIC_ hoặc VITE_ có nghĩa gì? Back: Biến đó bị nhúng vào bundle phía client, ai mở DevTools cũng đọc được.

Front: Repo private có an toàn để chứa secret không? Back: Không, vẫn có nhiều người và công cụ đọc được, và lịch sử git thì khó xóa sạch.

Front: Cách chặn secret lọt vào repo về lâu dài? Back: Thêm .gitignore đúng và bật secret scanning trong CI trước khi merge.

Front: AI thường quên gì khi dùng biến môi trường? Back: Kiểm tra biến có tồn tại không, khiến app chạy với undefined mà không báo lỗi rõ.

Front: Cấu hình khác nhau giữa local, staging và production nên đặt ở đâu? Back: Trong biến môi trường của từng môi trường, không sửa code.