Cover image
Phan Trung Nghĩa
Phan Trung Nghĩa
Senior Fullstack Developer

Security Debt: Khi Technical Debt Có Thể Giết Chết Startup Của Bạn

15/06/2026
38 views

2


Hai startup, hai kết cục

Startup A ra mắt MVP trong 6 tuần. Auth bằng JWT tự viết, password hash bằng MD5 vì "nhanh hơn bcrypt", session không có expiry, API endpoint không check authorization — chỉ check authentication. Họ lý luận: "Chưa có user thật, chưa cần lo security."
Tháng thứ 4, họ có 8,000 user. Tháng thứ 5, có người tìm ra IDOR (Insecure Direct Object Reference) trong API — chỉ cần đổi user_id trong request là đọc được data của bất kỳ ai. Toàn bộ database 8,000 user bị scrape trong một đêm. Email, số điện thoại, lịch sử giao dịch.
Họ mất 3 tháng và gần như toàn bộ runway để xử lý hậu quả — thông báo user, rewrite auth layer, audit lại toàn bộ API. Một co-founder rời đi. Họ không bao giờ recover được trust từ những early adopter quan trọng nhất.
Startup B cũng làm nhanh. Nhưng trước khi viết dòng code đầu tiên, họ dành 2 giờ để trả lời một câu hỏi: "Nếu hệ thống của chúng ta bị tấn công, thứ tệ nhất có thể xảy ra là gì?" Từ đó, họ xác định 4 security invariants — những property bắt buộc phải đúng từ đầu — và xây mọi thứ xung quanh đó. Phần còn lại, họ iterate như bình thường.
Startup B vẫn ship nhanh. Nhưng họ không bao giờ phải dừng lại để rebuild nền móng.
Sự khác biệt không phải là bao nhiêu thời gian họ dành cho security. Sự khác biệt là họ hiểu cái gì không thể sửa sau.

Technical Debt — Khái Niệm Bạn Biết Nhưng Chưa Hiểu Đủ

Ward Cunningham — người đặt ra thuật ngữ "technical debt" — mô tả nó như một khoản vay: bạn viết code chưa hoàn hảo hôm nay để ship nhanh hơn, với ý định sẽ "trả nợ" sau bằng cách refactor.
Metaphor này có hai phần quan trọng mà developer hay bỏ qua:
Một là: nợ có lãi suất. Code xấu không chỉ là code xấu — nó là code ngày càng khó sửa hơn khi codebase lớn dần, khi có thêm người join, khi có thêm feature build trên nền đó. Lãi suất của technical debt tăng theo thời gian.
Hai là: có những khoản nợ không thể trả. Bạn có thể refactor một function xấu. Bạn có thể đổi tên biến, tách class, viết lại module. Nhưng có một số quyết định kiến trúc — một khi đã đi theo hướng đó và build đủ nhiều thứ lên trên — thì chi phí để quay lại gần như tương đương với việc rewrite từ đầu.
Security debt thuộc loại thứ hai.

Security Debt Khác Technical Debt Thông Thường Ở Điểm Gì

Đây là điểm mấu chốt mà hầu hết bài viết về chủ đề này bỏ qua.
Technical debt thông thường ảnh hưởng đến tốc độ của bạn. Codebase càng nhiều nợ, bạn develop càng chậm, bug xuất hiện càng nhiều, onboarding engineer mới càng mất thời gian. Đau, nhưng là đau từ từ, có thể dự báo được.
Security debt ảnh hưởng đến sự tồn tại của bạn. Và nó hoạt động theo cơ chế hoàn toàn khác:
Cơ chế 1: Rủi ro không tuyến tính
Technical debt tích lũy đều đặn — 10 bad function hôm nay, 20 bad function tuần sau. Security debt không hoạt động theo cách đó. Một lỗ hổng duy nhất, dù hệ thống của bạn có 99 thứ khác hoàn hảo, vẫn có thể là điểm vào để toàn bộ hệ thống sụp đổ.
Attacker không cần phá vỡ mọi thứ. Họ chỉ cần tìm đúng một điểm yếu.

Cơ chế 2: Chi phí tăng theo cấp số nhân theo thời gian phát hiện
IBM Cost of a Data Breach Report (2023) đưa ra con số cụ thể: trung bình một data breach tốn $4.45 triệu USD để xử lý — bao gồm investigation, remediation, notification, legal liability, và reputational damage.
Nhưng con số quan trọng hơn là thời gian phát hiện: những breach được phát hiện trong vòng 200 ngày có chi phí trung bình thấp hơn 23% so với những breach phát hiện sau 200 ngày. Lỗ hổng càng tồn tại lâu, thiệt hại càng nhân lên.
Với startup giai đoạn đầu, bạn không có $4.45 triệu để xử lý hậu quả. Một breach nghiêm trọng ở scale vài nghìn user đã đủ để:
  • Mất toàn bộ early adopter
  • Vi phạm quy định bảo vệ dữ liệu (PDPA tại Việt Nam, GDPR nếu có user EU)
  • Mất khả năng raise funding — không investor nào muốn đổ tiền vào startup vừa bị hack
  • Mất nhân sự — engineer giỏi không muốn làm việc với codebase mà họ biết là thiếu an toàn
Cơ chế 3: Trust collapse không có đường quay lại
Technical debt không ai nhìn thấy từ bên ngoài. Security incident thì ngược lại — nó là public event. Một khi user đã bị breach, họ không quên. Và trong thời đại social media, câu chuyện lan rất nhanh.
Đây là thứ không thể mua lại bằng tiền hay refactor bằng code.

Barry Boehm và Cái Giá Của Việc Sửa Muộn

Năm 1981, nhà khoa học máy tính Barry Boehm công bố một nghiên cứu mà kết quả của nó vẫn còn nguyên giá trị đến hôm nay: chi phí để sửa một lỗi tăng theo hàm mũ theo thời gian phát hiện trong vòng đời phát triển phần mềm.
Cụ thể:
  • Sửa lỗi trong giai đoạn requirements/design: chi phí cơ sở = 1x
  • Sửa trong giai đoạn coding: 6x
  • Sửa trong giai đoạn testing: 15x
  • Sửa sau khi đã release: 100x
Boehm gọi đây là Cost of Change Curve — và nó là lý do cơ bản nhất tại sao "làm đúng từ đầu" không phải là chủ nghĩa hoàn hảo, mà là kinh tế học thuần túy.
Với security, curve này còn dốc hơn. Vì sửa security không chỉ là sửa code — đó là sửa kiến trúc, migrate data, thay đổi cách hệ thống xử lý mọi request, retest toàn bộ, và đôi khi thông báo cho tất cả user về việc thay đổi.
Lấy ví dụ cụ thể: Row-Level Security (RLS).
Nếu bạn thiết kế database từ đầu với RLS — mỗi query tự động filter theo user_id của người đang đăng nhập — đó là vài dòng config trong PostgreSQL và một số convention trong ORM layer. Chi phí: gần bằng 0.
Nếu bạn đã có 50 model, 200 API endpoint, và data của nhiều user đã pha trộn trong cùng table mà không có isolation rõ ràng, việc retrofit RLS là một dự án riêng kéo dài vài tuần, với rủi ro cao là bỏ sót edge case nào đó trong quá trình migration.
Đây không phải lý thuyết — đây là câu chuyện xảy ra ở hầu hết mọi startup khi họ bắt đầu nghĩ nghiêm túc về multi-tenancy hoặc data isolation.

Ba Quyết Định Kiến Trúc Mà Solo Builder Thường Làm Sai

Không phải mọi security decision đều quan trọng như nhau ở giai đoạn đầu. Nhưng có ba quyết định mà nếu làm sai, chi phí để sửa sau này là không tỷ lệ với công sức làm đúng ngay từ đầu.
Quyết định 1: Authentication & Session Management
Đây là nơi nhiều solo builder tự viết thay vì dùng thư viện đã được kiểm chứng — vì nghĩ là "đơn giản thôi."
Auth không đơn giản. Auth là một trong những domain có nhiều edge case và subtle vulnerability nhất trong toàn bộ application security. Chỉ riêng JWT đã có hàng chục cách implement sai: alg: none attack, weak secret, missing expiry, improper invalidation sau logout, không rotate refresh token.
Làm đúng từ đầu trông như thế nào:
  • Dùng thư viện auth đã được audit: NextAuth, Passport.js, Auth.js, hoặc managed service như Clerk, Supabase Auth
  • Password phải hash bằng bcrypt/argon2 — không phải MD5, SHA1, hay SHA256 thuần túy
  • Session token phải có expiry hợp lý và phải invalidate được server-side khi logout
  • Implement rate limiting trên login endpoint từ ngày đầu — brute force là attack cơ bản nhất
Chi phí để làm đúng: thêm 2-3 giờ setup ban đầu.
Chi phí để fix sau khi đã có user và đang dùng auth sai: migration toàn bộ password hash (buộc user reset password), rewrite session management, audit toàn bộ flow liên quan. Tính bằng tuần.

Quyết định 2: Authorization Model — Ai Được Xem Gì
Authentication hỏi: "Bạn là ai?" Authorization hỏi: "Bạn được phép làm gì?"
Hầu hết solo builder implement authentication tương đối ổn. Authorization thì thường bị bỏ qua hoặc làm không nhất quán.
Pattern nguy hiểm phổ biến nhất: check authentication nhưng không check authorization.
Code
Copied!
// ❌ Pattern sai — chỉ check user đã login
app.get('/api/documents/:id', requireAuth, async (req, res) => {
  const doc = await Document.findById(req.params.id)
  return res.json(doc)
})

// ✅ Pattern đúng — check user có quyền với resource cụ thể
app.get('/api/documents/:id', requireAuth, async (req, res) => {
  const doc = await Document.findOne({
    _id: req.params.id,
    owner: req.user.id  // Chỉ trả về nếu document thuộc về user này
  })
  if (!doc) return res.status(404).json({ error: 'Not found' })
  return res.json(doc)
})
Lỗi này có tên trong OWASP Top 10: Broken Object Level Authorization (BOLA) — hay còn gọi là IDOR (Insecure Direct Object Reference). Đây là vulnerability phổ biến nhất trong API hiện đại, và là thứ đã hạ gục Startup A trong câu chuyện mở đầu.
Làm đúng từ đầu: Mọi query lấy data đều phải có điều kiện filter theo owner/tenant. Đây là convention — một khi đã là convention trong codebase, mọi developer (kể cả bạn trong tương lai) sẽ tự nhiên follow.
Fix sau: Audit lại 200 API endpoint, tìm chỗ nào còn thiếu authorization check, test từng cái. Và bạn sẽ không bao giờ chắc chắn 100% mình đã tìm hết.
Quyết định 3: Secret Management
Secrets — API key, database password, JWT secret, third-party credentials — là mục tiêu số một của automated scanner trên GitHub. Có những tool chạy 24/7 scan mọi public repository mới để tìm credentials bị commit nhầm.
Pattern sai phổ biến:
Code
Copied!
# .env file bị commit vào git
DATABASE_URL=postgresql://admin:supersecret@localhost/prod
JWT_SECRET=myverysecretkey123
STRIPE_SECRET_KEY=sk_live_xxxxx
Hoặc hardcode trong code:
Code
Copied!
const stripe = new Stripe('sk_live_xxxxx') // ❌
Làm đúng từ đầu:
  • .env phải có trong .gitignore từ commit đầu tiên — không phải "sẽ thêm sau"
  • Dùng environment variables hoặc secret manager (AWS Secrets Manager, Doppler, Vault)
  • Rotate tất cả credentials nếu bạn không chắc chắn 100% chưa bao giờ commit
Chi phí để làm đúng: 30 phút setup .gitignore và .env.example đúng cách.
Chi phí khi bị lộ: rotate toàn bộ credential, audit xem secret đó đã bị dùng chưa, thông báo cho các bên liên quan. Stripe key bị lộ có thể dẫn đến charge fraudulent hàng nghìn đô trước khi bạn kịp nhận ra.

Security Invariants — Khái Niệm Bạn Cần Biết

Đây là framework tôi thấy hữu ích nhất để solo builder tiếp cận security mà không bị overwhelm.
Security invariant là một property của hệ thống phải luôn đúng, bất kể codebase phát triển theo hướng nào.
Ví dụ:
  • "Không có user nào có thể đọc data của user khác trừ khi được explicit grant."
  • "Mọi input từ bên ngoài đều phải được validate trước khi xử lý."
  • "Không có credential nào được hardcode trong source code."
  • "Mọi action thay đổi data phải được log với timestamp và actor."
Điểm mạnh của cách tiếp cận này: thay vì cố gắng implement một checklist security dài vô tận, bạn xác định 4-5 invariant quan trọng nhất với domain của mình, rồi đảm bảo mọi quyết định thiết kế không vi phạm chúng.
Và khi onboard người mới hoặc review PR, câu hỏi không phải là "code này có secure không?" (quá mơ hồ) mà là "code này có vi phạm invariant nào không?" (có thể kiểm tra được).
Quy trình xác định security invariants cho solo builder:
  1. Hỏi: "Data nhạy cảm nhất trong app của tôi là gì?" (PII, payment info, private content)
  2. Hỏi: "Thứ tệ nhất có thể xảy ra nếu data đó bị lộ?"
  3. Hỏi: "Điều gì phải luôn đúng để điều đó không xảy ra?"
  4. Viết ra 4-5 câu khẳng định — đó là invariants của bạn
  5. Review lại chúng mỗi khi thêm feature mới có ảnh hưởng đến data model

Vibe Coding + Security Debt — Nhân Tố Nhân

Một yếu tố mới cần nhắc đến trong năm 2025: AI-assisted development đang làm cho security debt tích lũy nhanh hơn bao giờ hết.
Lý do không phải AI tạo ra code xấu (dù điều đó cũng xảy ra — nghiên cứu của NYU/Stanford năm 2022 cho thấy ~40% code do Copilot generate trong security-sensitive context chứa lỗ hổng). Lý do sâu hơn là velocity tăng nhưng comprehension không tăng tương ứng.
Khi bạn tự viết code, bạn hiểu từng dòng. Khi bạn vibe code với AI, bạn có thể review và hiểu — nhưng dưới áp lực deadline, review trở thành "đọc lướt thấy trông ổn thì ship."
Với business logic, đó là acceptable risk — bug có thể fix bằng hotfix.
Với security, một authorization check bị bỏ sót, một input validation thiếu, một secret bị log — những thứ này có thể nằm im trong production nhiều tháng trước khi được exploit.
Nguyên tắc thực tế: Với mọi code liên quan đến auth, authorization, input handling, hoặc data access — dù do AI hay bạn viết — hãy dành thêm 5 phút để hỏi: "Code này có vi phạm security invariant nào của mình không?"
Năm phút đó là khoản đầu tư tốt nhất bạn có thể làm.

Minimal Security Baseline Cho Solo Builder

Không ai expect bạn implement zero-trust architecture hay ISO 27001 từ ngày đầu. Nhưng đây là baseline tối thiểu mà chi phí implement thấp hơn rất nhiều so với chi phí bỏ qua:
Trước khi viết dòng code đầu tiên (2 giờ):
  • Xác định 4-5 security invariants cho domain của bạn
  • Setup .gitignore đúng cách, không bao giờ commit .env
  • Chọn managed auth solution thay vì tự viết
Khi build data layer:
  • Mọi query lấy data đều filter theo owner — đây là convention, không phải option
  • Dùng ORM với parameterized query — không bao giờ string concatenation trong SQL
  • Thiết kế data model với tư duy "ai được phép xem table này?"
Khi build API layer:
  • Authentication + Authorization tách biệt và nhất quán
  • Validate input ở layer nhận — không tin tưởng bất kỳ thứ gì từ client
  • Rate limiting trên mọi endpoint public, đặc biệt auth endpoint
Trước khi go live với user thực:
  • Chạy OWASP ZAP hoặc tương đương để scan một lần
  • Review tất cả environment variable — không có gì hardcode
  • Có kế hoạch "nếu bị breach, tôi làm gì trong 24 giờ đầu?"

Câu Hỏi Đúng Không Phải Là "Khi Nào Làm Security"

Câu hỏi sai: "Nên làm security ở giai đoạn nào?"
Câu hỏi đúng: "Quyết định nào hôm nay sẽ không thể sửa lại sau mà không phải trả giá rất đắt?"
Với technical debt thông thường — code xấu, thiếu test, architecture chưa tốt — câu trả lời thường là "ship trước, refactor sau." Và đó là quyết định hợp lý trong nhiều trường hợp.
Với security debt — auth sai, authorization thiếu, secret bị lộ, data không được isolate — câu trả lời là khác. Vì những thứ này không chỉ làm bạn chậm. Chúng có thể kết thúc mọi thứ bạn đang xây dựng.
Solo builder giỏi không phải là người làm security hoàn hảo từ đầu. Mà là người biết phân biệt được: thứ gì có thể sửa sau, và thứ gì không.
Đó là tư duy của người build để tồn tại — không chỉ build để ship.
2