Những biến môi trường bạn đang vô tình lộ ra Frontend mà không hề hay biết

05 tháng 4, 2026·6 phút đọc

Bạn thêm tiền tố NEXT_PUBLIC_ vào API key "chỉ để test nhanh thôi". Sáu tháng trôi qua, nó vẫn nằm đó. Đây là cách những lỗ hổng bảo mật âm thầm xuất hiện trong mã nguồn của bạn.

Những biến môi trường bạn đang vô tình lộ ra Frontend mà không hề hay biết

Bạn thêm tiền tố NEXT_PUBLIC_ vào API key "chỉ để test nhanh thôi". Sáu tháng trôi qua, nó vẫn nằm đó.

Hầu hết các lập trình viên đều nắm rõ quy tắc: các khóa bí mật (secret keys) phải nằm trong file .env, tuyệt đối không được đưa vào mã phía client (client code). Tuy nhiên, thực tế các vụ lộ dữ liệu thường không rõ ràng như vậy. Chúng không xảy ra vì sự cẩu thả, mà do công cụ phát triển (tooling) gây nhầm lẫn, thông báo lỗi quá im lặng, và những sai sót ban đầu trông có vẻ hoàn toàn bình thường cho đến khi ai đó mở DevTools hoặc kiểm tra bundle của bạn.

Minh họa về bảo mật mã nguồnMinh họa về bảo mật mã nguồn

Lỗi 1 — Tiền tố NEXT_PUBLIC_ cho các biến bí mật

Next.js có thiết kế sẽ lộ bất kỳ biến nào có tiền tố NEXT_PUBLIC_ ra trình duyệt. Vấn đề nằm ở chỗ, các lập trình viên thường nghĩ ngay đến tiền tố này ngay khi gặp lỗi "variable is undefined" ở phía client — mà không dừng lại hỏi tại sao nó lại bị undefined.

# .env.local

# An toàn — cái này được định nghĩa là công khai
NEXT_PUBLIC_API_URL=https://api.example.com

# NGUY HIỂM — giờ bị lộ trong mọi JS bundle
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxxxxxxxx
NEXT_PUBLIC_DATABASE_URL=postgres://user:password@host/db

Bất kỳ ai cũng có thể mở trang web đã triển khai của bạn, vào phần Network → JS files, tìm kiếm sk_live_ và thấy được nó.

Khóa bí mật của Stripe có một tiền tố rất dễ nhận biết. Tương tự cho AWS access keys (AKIA), Supabase service role keys, và SendGrid API keys.

Cách khắc phục: Nếu khóa của bạn chỉ được dùng trong API routes hoặc server components, nó tuyệt đối không được có tiền tố NEXT_PUBLIC_. Hãy gọi một route phía backend thay thế.

// Sai — khóa bí mật bị lộ ra client
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY!);

// Đúng — chỉ dùng trong /api/checkout/route.ts (server-side)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

Lỗi 2 — import.meta.env của Vite và tiền tố VITE_

Vite hoạt động theo cơ chế hoàn toàn tương tự. Bất kỳ biến nào có tiền tố VITE_ đều được "inline" tĩnh vào bundle của bạn tại thời điểm build. Nó không được fetch tại thời điểm chạy (runtime) — nó được sao chép y nguyên vào JavaScript của bạn.

VITE_API_URL=https://api.example.com        # ổn
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1Ni   # ổn
VITE_SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1Ni...  # không bao giờ dùng tiền tố này

Hãy chạy npm run build, sau đó mở file dist/assets/index-xxxx.js và tìm kiếm khóa của bạn. Bạn sẽ thấy nó hiện diện dưới dạng văn bản thuần (plain text) ngay trong mã sản xuất.

Lỗi 3 — process.env trong mã client đã được bundle

Với Create React App hoặc các cấu hình webpack cũ hơn, các biến REACT_APP_ sẽ được inline. Tuy nhiên, ngay cả khi không dùng mẫu này, lập trình viên đôi khi viết các file tiện ích dùng chung (shared utility files) bị kéo vào client bundle mà không hay biết.

// utils/config.ts — được import bởi cả client và server code
export const config = {
  apiUrl: process.env.API_URL,
  stripeKey: process.env.STRIPE_SECRET_KEY,
};

Mẫu an toàn: Không bao giờ import config server vào code được chia sẻ với client.

// lib/server-config.ts — chỉ được import trong các file server
export const serverConfig = {
  apiUrl: process.env.API_URL,
  stripeKey: process.env.STRIPE_SECRET_KEY,
};

// lib/client-config.ts — an toàn để import ở bất cứ đâu
export const clientConfig = {
  apiUrl: process.env.NEXT_PUBLIC_API_URL,
};

Lỗi 4 — Lộ .env thông qua Source Maps

Bạn đã cẩn thận với các biến của mình. Nhưng có phải bạn đã đẩy source maps lên môi trường sản xuất (production)? Source maps tái tạo mã nguồn gốc của bạn trong trình duyệt. Nếu mã phía server của bạn kết thúc trong một source map ở bản build production, tab DevTools Sources sẽ hiển thị file gốc — bao gồm cả các giá trị dự phòng (fallbacks) được hardcode.

// Một mẫu phổ biến nhưng đầy rủi ro
const key = process.env.STRIPE_SECRET_KEY || "sk_live_fallback_for_dev";

Trong Next.js, hãy đảm bảo source maps bị tắt trong môi trường production:

// next.config.js
module.exports = {
  productionBrowserSourceMaps: false, // mặc định là false — hãy đảm bảo nó giữ nguyên
};

Đối với Vite:

// vite.config.ts
export default defineConfig({
  build: {
    sourcemap: false, // hoặc "hidden" nếu bạn cần cho việc theo dõi lỗi
  },
});

Lỗi 5 — Route API debug bạn quên xóa

Điều này hơi ngại nhưng lại rất phổ biến. Trong quá trình debug local, bạn thêm một route nhanh để d dumping trạng thái env. Bạn commit nó. Và nó được đẩy lên production.

https://yourapp.com/api/debug

Bây giờ nó trả về mọi biến môi trường trên server của bạn.

// pages/api/debug.ts
export default function handler(req, res) {
  res.json({ env: process.env }); // tuyệt đối không được đưa lên production
}

Cách kiểm tra những gì bạn thực sự đang triển khai

Hãy build ứng dụng và dùng grep để tìm các mẫu bí mật đã biết trong bundle:

# Next.js
npm run build
grep -r "sk_live\|AKIA\|password\|secret" .next/static/chunks/

# Vite
npm run build
grep -r "sk_live\|AKIA\|password\|secret" dist/assets/

Các sai lầm phổ biến khác

  • Thêm tiền tố cho các bí mật "tạm thời" để sửa lỗi undefined phía client — nguyên nhân gốc rễ mang tính kiến trúc, không phải do thiếu tiền tố.
  • Chia sẻ một file config duy nhất giữa code server và client — hãy tách biệt chúng và thực thi điều này bằng các quy tắc import của ESLint.
  • Sử dụng fallbacks với khóa thật — process.env.KEY || "real-key-here" làm vô hiệu hóa toàn bộ mục đích bảo mật.
  • Quên rằng các SDK bên thứ ba được khởi tạo phía client sẽ ghi log cấu hình của chúng (Sentry, Supabase, Firebase nếu khởi tạo sai khóa sẽ lộ chúng trong các network request).

Bài học rút ra

Những vụ rò rỉ gây hại cho bạn không phải là những điều hiển nhiên — mà là những thứ tinh vi. Đó là mỗi lần bạn thấy NEXT_PUBLIC_ thêm vội vã, file config chung bị bundle, hay route debug lọt vào production. Hãy xây dựng thói quen: trước mỗi lần deploy, hãy grep bundle của bạn theo các mẫu bí mật đã biết, giữ sự tách biệt nghiêm ngặt giữa config server và client, và hãy coi mọi biến môi trường là "có tội" cho đến khi được chứng minh là an toàn để lộ ra.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗