← DevSecOps← DevSecOps
DevSecOpsDevSecOps19 Th7, 2026Jul 19, 202628 phút đọc21 min read

Secure Coding & Bảo mật ứng dụng WebSecure Coding & Web Application Security

Thuộc bộ kiến thức DevSecOps Roadmap.

Tổng quan

Secure coding là thực hành viết phần mềm chống chịu được lạm dụng nhờ thiết kế, chứ không phải nhờ may mắn. Phần lớn các vụ vi phạm (breach) không bắt đầu từ một zero-day kỳ lạ — chúng bắt đầu từ một câu query được ghép chuỗi (string concatenation), một template render HTML do attacker kiểm soát mà không escape, một endpoint thiếu kiểm tra authorization, hoặc một dependency có CVE đã biết mà không ai cập nhật. Đây đều là những lỗi coding thông thường, và mỗi lỗi đều có thể phòng ngừa bằng các biện pháp phòng thủ đã được hiểu rõ, mang tính cơ học: parameterized queries, output encoding, allowlisting, và least privilege được thực thi ở mọi tầng.

Note này chỉ mang tính phòng thủ (defensive). Với mỗi lớp lỗ hổng, chúng ta mô tả cơ chế thất bại (failure mode) vừa đủ để nhận diện, sau đó luôn đưa ra biện pháp phòng thủ cụ thể để vô hiệu hóa nó — code an toàn đặt cạnh code không an toàn, trong mọi trường hợp. Mục tiêu là một developer có thể nhìn vào một diff và ngay lập tức nhận ra “đoạn này ghép chuỗi input người dùng vào query” hay “đoạn này render raw HTML”, và biết cách sửa chỉ trong một dòng.

DevSecOps xem đây là trách nhiệm của tất cả mọi người, không phải một cổng kiểm soát (gate) ở cuối quy trình. Secure coding practices, quét tự động (SAST cho code, SCA cho dependency), và code review là các biện pháp kiểm soát chạy bên trong vòng lặp phát triển — dịch chuyển bảo mật sang trái (shift left) để lỗ hổng được phát hiện khi còn rẻ để sửa, chứ không phải sau khi đã lên production. Note này tập trung vào tầng application code; việc quét ở tầng pipeline và kiểm soát supply chain được trình bày trong CI/CD & Supply Chain Security, còn cách ưu tiên rủi ro nào quan trọng nhất cho hệ thống của bạn được trình bày trong Threat Modeling & Risk Assessment.

Kiến thức nền tảng

Nguyên tắc secure coding cốt lõi

Một số ít nguyên tắc tạo ra phần lớn các quy tắc cụ thể mà bạn sẽ áp dụng. Nắm vững những nguyên tắc này, bạn có thể tự suy ra biện pháp phòng thủ đúng cho một lớp lỗ hổng bạn chưa từng gặp.

Nguyên tắcÝ nghĩaVí dụ áp dụng
Không bao giờ tin input (never trust input)Mọi dữ liệu đi qua ranh giới tin cậy (trust boundary) — input người dùng, API request, file upload, response từ bên thứ ba, environment variable — đều bị coi là thù địch cho đến khi được chứng minh ngược lạiValidate và encode toàn bộ request parameters, headers, cookies, thậm chí cả dữ liệu đọc lại từ chính database của bạn nếu nó có thể bị user khác đầu độc
Fail securely (thất bại một cách an toàn)Khi có lỗi xảy ra, phải fail closed (từ chối) — không phải fail open (cho phép)Một kiểm tra authorization ném exception phải từ chối truy cập, không được âm thầm cho request đi qua
Least privilege (đặc quyền tối thiểu)Mọi component, user, process chỉ được cấp quyền tối thiểu cần thiết, không hơnTài khoản service cho báo cáo chỉ có quyền SELECT, không bao giờ có DROP hay DELETE; IAM role của một Lambda function chỉ giới hạn trong đúng một bucket nó cần
Secure defaults (mặc định an toàn)Cấu hình mặc định khi mới cài đặt phải là cấu hình an toàn; hành vi không an toàn phải được bật lên một cách tường minhAPI endpoint mới yêu cầu authentication trừ khi được đánh dấu public một cách tường minh; cookie mặc định có Secure, HttpOnly, SameSite=Lax
Defense in depth (phòng thủ theo chiều sâu)Xếp chồng nhiều lớp kiểm soát độc lập để một lớp thất bại không đồng nghĩa với một vụ breachInput validation parameterized queries tài khoản DB least-privilege WAF — không lớp nào một mình là “biện pháp duy nhất”
Giảm thiểu attack surfaceMỗi endpoint, feature flag, trường dữ liệu được expose đều là thứ có thể bị tấn công — loại bỏ những gì không cần thiếtTắt directory listing, gỡ bỏ debug endpoint trước khi release, không expose internal ID hay stack trace
Complete mediation (kiểm tra đầy đủ mọi lần)Kiểm tra authorization ở mọi lần truy cập, mọi lần — không bao giờ cache “đã kiểm tra rồi” giữa các request hoặc các object khác nhauXác minh lại quyền sở hữu order_id=123 ở mỗi request, kể cả khi user đó vừa được cho phép truy cập một order khác một phút trước (đây chính là điều ngăn IDOR)
Phân tách trách nhiệm (separation of duties)Không một cá nhân hay service nào (con người hay máy) được vừa tạo ra vừa tự phê duyệt một thay đổi quan trọng mà không có kiểm soátYêu cầu code review trước khi merge; pipeline CI thực hiện deploy không phải cùng một identity có quyền approve PR

Đây không phải là những khẩu hiệu trừu tượng — mỗi nguyên tắc ánh xạ trực tiếp tới một biện pháp kiểm soát cụ thể ở cấp code, được trình bày ở phần sau.

OWASP Top 10 (2021)

OWASP Top 10 là danh sách rút gọn chuẩn ngành về những rủi ro ứng dụng web có khả năng gây ra breach thực tế cao nhất. Đây là công cụ ưu tiên hóa, không phải checklist đầy đủ — hãy coi nó là mức sàn tối thiểu.

#CategoryMô tả ngắn gọnBiện pháp phòng thủ chính
A01Broken Access ControlUser có thể hành động vượt ra ngoài quyền hạn dự định (xem/sửa dữ liệu người khác, leo thang quyền)Deny by default; thực thi authorization phía server ở mọi request; tập trung hóa việc kiểm tra, không rải rác nhiều nơi
A02Cryptographic FailuresDữ liệu nhạy cảm bị lộ do mã hóa yếu, thiếu, hoặc sử dụng sai (lưu plaintext, cipher yếu, không dùng TLS)Mã hóa dữ liệu khi truyền (TLS 1.2+) và khi lưu trữ; dùng thuật toán và thư viện đã được kiểm chứng, không bao giờ tự viết crypto
A03InjectionDữ liệu không tin cậy được gửi tới một interpreter (SQL, OS shell, LDAP, NoSQL) và bị thực thi như code/lệnhParameterized queries / prepared statements; escape hoặc tránh hoàn toàn interpreter; validate input
A04Insecure DesignThiếu hoặc không hiệu quả về mặt kiểm soát bảo mật ngay từ kiến trúc, không chỉ ở phần triển khaiThreat modeling trong giai đoạn thiết kế; secure design pattern; abuse-case testing
A05Security MisconfigurationCredential mặc định, thông báo lỗi quá chi tiết, tính năng không cần thiết được bật, thiếu security headerChuẩn cấu hình đã hardening và tự động hóa; tắt những gì không dùng; secure defaults
A06Vulnerable and Outdated ComponentsSử dụng thư viện/framework có lỗ hổng đã biết, hoặc phiên bản không còn được hỗ trợ/EOLQuét SCA trong CI; duy trì tần suất patch; loại bỏ dependency không dùng
A07Identification and Authentication FailuresQuản lý session yếu, credential stuffing, thiếu MFA, session token dễ đoánQuản lý session chặt chẽ, MFA, giới hạn tốc độ đăng nhập (rate limit), hash mật khẩu an toàn (bcrypt/argon2)
A08Software and Data Integrity FailuresTin tưởng code/plugin/CI-CD pipeline chưa được ký hoặc xác minh (bao gồm cả insecure deserialization)Xác minh chữ ký/checksum, dùng SBOM, khóa phiên bản dependency, bảo vệ pipeline CI/CD
A09Security Logging and Monitoring FailuresVi phạm không bị phát hiện vì không có gì được log, cảnh báo, hoặc giám sátLog tập trung các sự kiện auth và lỗi access-control; alerting; lưu trữ audit trail
A10Server-Side Request Forgery (SSRF)Server bị lừa gửi request tới một đích không mong muốn (thường là nội bộ)Allowlist đích outbound; tắt scheme/redirect URL không dùng; phân đoạn mạng (network segment) cho service nội bộ

Danh sách này thay đổi theo thời gian (OWASP Top 10 được cập nhật khoảng mỗi 3-4 năm) — luôn xác nhận bạn đang tham chiếu đúng phiên bản hiện hành.

Khái niệm chính

Phòng ngừa Injection: SQL injection

SQL injection xảy ra khi input không tin cậy được ghép trực tiếp vào chuỗi query, cho phép attacker thay đổi logic của query. Cách khắc phục mang tính cấu trúc, không phải bề mặt: không bao giờ xây dựng query bằng cách ghép chuỗi (string-concatenate) input người dùng. Hãy dùng parameterized queries (prepared statements) để database luôn coi input là dữ liệu, không bao giờ là code, bất kể input chứa ký tự gì.

# KHÔNG AN TOÀN — ghép chuỗi cho phép input thay đổi cấu trúc query
def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    cursor.execute(query)
    return cursor.fetchone()

# AN TOÀN — parameterized query; driver gửi input như một giá trị bound,
# không bao giờ là một phần của SQL text
def get_user(username):
    query = "SELECT * FROM users WHERE username = %s"
    cursor.execute(query, (username,))
    return cursor.fetchone()
// KHÔNG AN TOÀN — Statement thực thi bất cứ chuỗi nào bạn đưa vào
String query = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);

// AN TOÀN — PreparedStatement bind tham số tách biệt khỏi SQL text
String query = "SELECT * FROM users WHERE username = ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();

ORM không tự động an toàn. Các ORM (SQLAlchemy, Hibernate, ActiveRecord, Sequelize, Prisma) parameterize các query được xây dựng qua query API của chúng theo mặc định — nhưng mọi ORM đều cung cấp một “cửa hậu” (escape hatch) cho raw SQL, và cửa hậu đó không an toàn hơn gì so với SQL viết tay nếu bạn ghép chuỗi vào nó.

// KHÔNG AN TOÀN — cửa hậu raw query, string-interpolated
await sequelize.query(`SELECT * FROM users WHERE email = '${email}'`);

// AN TOÀN — cùng cửa hậu raw query đó, nhưng đã parameterize
await sequelize.query(
  "SELECT * FROM users WHERE email = :email",
  { replacements: { email }, type: QueryTypes.SELECT }
);

// AN TOÀN NHẤT — dùng query builder / ORM API, tự động parameterize
await User.findOne({ where: { email } });

Các lớp phòng thủ bổ sung: validate hình dạng (shape) của input trước khi nó chạm tới query (một field email phải trông giống một email), và chạy query dưới một tài khoản database least-privilege không thể DROP TABLE hay đọc các bảng không liên quan — để ngay cả khi injection thành công, thiệt hại vẫn bị giới hạn.

Injection không chỉ giới hạn ở SQL: nguyên tắc “không bao giờ ghép chuỗi input không tin cậy vào một interpreter” cũng áp dụng cho việc thực thi lệnh OS (dùng argument array / API như subprocess.run([...]) trong Python, không bao giờ dùng shell=True với input ghép chuỗi), LDAP query, NoSQL query object (một clause $where thô trong MongoDB cũng dễ bị injection như một chuỗi SQL), và template engine.

Phòng ngừa Cross-Site Scripting (XSS)

XSS xảy ra khi dữ liệu không tin cậy được render vào trang như thể nó là HTML/JavaScript đáng tin cậy, cho phép script của attacker thực thi trong phiên trình duyệt của người dùng khác. Có ba dạng: stored (payload được lưu ở phía server, ví dụ trong một comment, và phục vụ cho những người truy cập sau), reflected (payload được phản chiếu ngay lập tức trong response, ví dụ trang kết quả tìm kiếm), và DOM-based (lỗ hổng nằm hoàn toàn trong JavaScript phía client, ghi dữ liệu không tin cậy vào DOM, mà payload không nhất thiết phải chạm tới server).

Biện pháp phòng thủ chính là output encoding: encode dữ liệu theo đúng ngữ cảnh nó được render vào (HTML body, HTML attribute, chuỗi JavaScript, URL) tại điểm xuất ra (output), không phải tại điểm nhập vào (input).

// KHÔNG AN TOÀN — chèn HTML thô qua innerHTML
element.innerHTML = "Welcome, " + userSuppliedName;

// AN TOÀN — textContent không bao giờ diễn giải argument của nó như markup
element.textContent = "Welcome, " + userSuppliedName;
// KHÔNG AN TOÀN (React) — dangerouslySetInnerHTML làm đúng như tên gọi của nó
function Comment({ text }) {
  return <div dangerouslySetInnerHTML={{ __html: text }} />;
}

// AN TOÀN (React) — JSX tự động escape giá trị được interpolate theo mặc định
function Comment({ text }) {
  return <div>{text}</div>;
}
<!-- KHÔNG AN TOÀN (server-rendered template) — interpolation thô -->
<p>Search results for: {{{ query }}}</p>  <!-- triple braces = không escape ở nhiều engine -->

<!-- AN TOÀN — interpolation mặc định (tự động escape) -->
<p>Search results for: {{ query }}</p>

Các framework templating hiện đại tự động escape theo mặc định — Jinja2, Django templates, React JSX, Vue, Angular, và Rails ERB (với <%= %>) đều HTML-escape các giá trị được interpolate trừ khi bạn tường minh bỏ qua (|safe, dangerouslySetInnerHTML, v-html, {!! !!}). Lỗ hổng gần như luôn được tạo ra bởi việc chủ động bỏ qua mặc định của framework, vì vậy hãy coi mỗi lần dùng một helper “raw/unescaped” là một điểm cần review và giải trình thủ công.

DOM-based XSS cần được chú ý đặc biệt vì nó có thể xảy ra mà không cần server tham gia: JavaScript phía client đọc từ một nguồn (source) không tin cậy (location.hash, document.referrer, dữ liệu postMessage, tham số URL) và ghi nó vào một sink nguy hiểm (innerHTML, document.write, eval, setTimeout với argument là chuỗi, element.setAttribute("href", ...) với URL dạng javascript:). Cách khắc phục cũng theo cùng nguyên tắc, áp dụng phía client: không bao giờ đưa nguồn không tin cậy vào sink nguy hiểm mà không encode, và ưu tiên các sink an toàn (textContent, setAttribute cho các attribute không phải URL, các DOM API như createElement/appendChild thay vì xây dựng HTML bằng chuỗi).

// KHÔNG AN TOÀN — DOM-based XSS: location.hash (do attacker kiểm soát) đưa vào innerHTML
document.getElementById("greeting").innerHTML = decodeURIComponent(location.hash.slice(1));

// AN TOÀN — coi nó là text, không phải markup
document.getElementById("greeting").textContent = decodeURIComponent(location.hash.slice(1));

Content-Security-Policy (CSP) là một lớp phòng thủ thứ hai, độc lập: một HTTP response header giới hạn nguồn nào (script, style, resource khác) trình duyệt được phép tải và thực thi, để ngay cả khi một injection lọt qua, <script> inline hay payload host bên ngoài của attacker cũng đơn giản là không chạy được.

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'self'

Tránh dùng unsafe-inlineunsafe-eval trong script-src bất cứ khi nào có thể — chúng vô hiệu hóa phần lớn khả năng bảo vệ của CSP trước XSS. Ưu tiên allowlist dựa trên nonce hoặc hash cho những trường hợp hiếm khi bạn thực sự cần inline script:

Content-Security-Policy: script-src 'self' 'nonce-r4nd0mBase64PerRequest'

Các pattern validate input

Input validation là một biện pháp kiểm soát tại ranh giới tin cậy (trust boundary): nó thuộc về mọi điểm nơi dữ liệu đi từ một ngữ cảnh ít tin cậy hơn sang một ngữ cảnh tin cậy hơn — tại rìa API, trước khi một giá trị được dùng trong query, trước khi nó được ghi vào một đường dẫn file, trước khi nó được truyền cho một service downstream. Chỉ validate một lần tại API gateway rồi tin tưởng giá trị đó ở mọi nơi phía sau là một sai lầm phổ biến — service nội bộ và background job thường được gọi từ nhiều nơi hơn điểm vào ban đầu, theo thời gian hệ thống phát triển.

Allowlisting (positive validation) so với denylisting (negative validation):

Cách tiếp cậnCơ chế hoạt độngĐộ tin cậy
AllowlistĐịnh nghĩa chính xác những gì được phép (một pattern, một enum, một tập giá trị đã biết là tốt) và từ chối mọi thứ khácMạnh — tập input “xấu” là vô hạn và không thể liệt kê hết, nhưng tập input “tốt” thường nhỏ và đã biết
DenylistĐịnh nghĩa các pattern đã biết là xấu (ví dụ: chặn chuỗi <script>) và từ chối các trường hợp khớpYếu — attacker thường xuyên tìm ra các encoding/biến thể (<ScRiPt>, HTML entity, double-encoding) lọt qua denylist; hãy coi đây là một lớp phòng thủ bổ sung, không bao giờ là biện pháp kiểm soát chính

Ưu tiên allowlisting bất cứ khi nào không gian input hợp lệ có thể liệt kê được: mã quốc gia chỉ là một trong ~250 giá trị, tham số “sort order” chỉ là asc hoặc desc, phần mở rộng file cho upload chỉ là một trong số ít loại bạn tường minh hỗ trợ.

# KHÔNG AN TOÀN — denylist cố gắng chặn ký tự "xấu", dễ bị bypass
def is_safe_filename(name):
    return "../" not in name and ".." not in name  # bỏ sót nhiều encoding path traversal

# AN TOÀN — allowlist: chỉ chấp nhận một pattern chặt chẽ, đã biết là tốt
import re
def is_safe_filename(name):
    return bool(re.fullmatch(r"[a-zA-Z0-9_\-]{1,64}\.(png|jpg|pdf)", name))

Schema validation chính thức hóa allowlisting cho input có cấu trúc (JSON body, query string): định nghĩa chính xác hình dạng (shape), kiểu dữ liệu, khoảng giá trị, và trường bắt buộc bạn chấp nhận, và từ chối mọi thứ không tuân theo — trước khi bất kỳ business logic nào chạy.

// Ví dụ Node.js dùng thư viện schema validation (ví dụ: zod / joi / ajv)
const schema = z.object({
  email: z.string().email(),
  age: z.number().int().min(0).max(120),
  role: z.enum(["viewer", "editor", "admin"]),
});

app.post("/users", (req, res) => {
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ error: "Invalid input" });
  }
  // result.data giờ đã được validate và có type — an toàn để dùng phía sau
});

Validation là cần thiết nhưng bản thân nó không đủ — nó thu hẹp không gian input, nhưng output encoding và parameterized query vẫn cần thiết tại mọi sink, vì input “hợp lệ” (ví dụ một tên có chứa dấu nháy đơn hợp pháp như O'Brien) vẫn có thể nguy hiểm nếu sink không được phòng thủ độc lập. Đây chính là defense in depth: validate tại ranh giới, encode/parameterize tại sink.

Thiết kế API an toàn (secure API design)

API xứng đáng được chú ý đặc biệt vì chúng thường là bề mặt bị lộ nhiều nhất của một hệ thống và cũng là nơi kiểm tra authorization hay bị bỏ sót nhất. Nội dung chuyên sâu hơn về thiết kế API và cơ chế bảo mật web nằm ở Backend — APIsBackend — Web Security; tóm tắt liên quan tới DevSecOps:

// KHÔNG AN TOÀN — Mass Assignment: bất cứ gì client gửi lên đều được áp vào model
app.put("/api/users/:id", async (req, res) => {
  await User.update(req.body, { where: { id: req.params.id } });
});

// AN TOÀN — allowlist tường minh các trường được phép update; kiểm tra lại ownership mỗi request
app.put("/api/users/:id", requireAuth, async (req, res) => {
  if (req.user.id !== req.params.id && !req.user.isAdmin) {
    return res.status(403).json({ error: "Forbidden" });
  }
  const { displayName, bio } = req.body; // chỉ những trường này được chấp nhận
  await User.update({ displayName, bio }, { where: { id: req.params.id } });
});

Quản lý rủi ro dependency

Ứng dụng hiện đại được lắp ráp từ hàng chục đến hàng nghìn dependency mã nguồn mở (trực tiếp và gián tiếp), và A06: Vulnerable and Outdated Components của OWASP tồn tại chính vì bề mặt tấn công này rất lớn và thường ít được giám sát. Một lỗ hổng được công bố trong một thư viện được dùng rộng rãi (ví dụ một lỗi deserialization hoặc path traversal trong một framework logging) ngay lập tức trở thành lỗ hổng của mọi ứng dụng đóng gói phiên bản bị ảnh hưởng.

Software Composition Analysis (SCA) là công cụ quét manifest dependency (và thường cả artifact đã build) của bạn dựa trên các cơ sở dữ liệu lỗ hổng (NVD, GitHub Advisory Database, OSV) và gắn cờ các dependency có CVE đã biết, vấn đề license, và phiên bản lỗi thời.

Công cụEcosystemGhi chú
npm audit / yarn auditNode.jsTích hợp sẵn trong package manager; baseline miễn phí
pip-auditPythonQuét package đã cài / requirements file dựa trên PyPA advisory DB
DependabotGitHub-native, đa ecosystemTự động mở PR để nâng cấp phiên bản dependency có lỗ hổng
SnykĐa ecosystemSCA + quét container + IaC, thương mại có gói miễn phí
OWASP Dependency-CheckJava, .NET, và khácMã nguồn mở, thân thiện với CI, đối chiếu với NVD
TrivyContainer, IaC, filesystem, SCAMã nguồn mở, dùng rộng rãi trong pipeline CI

Vệ sinh thực hành ngoài việc quét: khóa phiên bản dependency (lockfile), loại bỏ dependency không dùng (mỗi dependency không dùng là attack surface không mang lại lợi ích gì), ưu tiên thư viện được bảo trì tốt với phản ứng bảo mật chủ động, và coi việc cập nhật dependency là một thao tác thường quy, rủi ro thấp bằng cách giữ khoảng cách giữa “lỗ hổng được công bố” và “bạn nâng cấp” càng nhỏ càng tốt — tự động hóa nó (Dependabot/Renovate) thay vì làm thủ công theo chu kỳ hàng quý.

Bảo mật supply chain & SBOM (tóm tắt)

Software Bill of Materials (SBOM) là một bản kê khai chính thức, có thể đọc bằng máy, liệt kê mọi thành phần (dependency trực tiếp và gián tiếp, phiên bản, và license của nó) cấu thành nên một phần mềm — về mặt khái niệm giống như danh sách thành phần trên bao bì thực phẩm đóng gói. Nó quan trọng vì bạn không thể đánh giá hay ứng phó với rủi ro trong những thành phần mà bạn không biết mình đang có: khi một CVE mới xuất hiện cho một thư viện, SBOM cho phép bạn trả lời “chúng ta có bị ảnh hưởng không, và ở đâu” trong vài phút bằng một truy vấn, thay vì mất nhiều ngày kiểm tra thủ công trên từng service.

Hai định dạng chủ đạo là SPDX (Software Package Data Exchange, một chuẩn ISO, ban đầu tập trung vào license) và CycloneDX (do OWASP duy trì, tập trung vào bảo mật, có phần mở rộng gốc cho vulnerability và provenance) — cả hai đều dựa trên JSON/XML và có thể được công cụ sinh ra tự động khi build. Việc sinh SBOM, ký (signing), và cách nó phù hợp trong bức tranh bảo mật supply chain rộng hơn (build provenance, artifact signing, các cấp độ SLSA) được trình bày chuyên sâu trong CI/CD & Supply Chain Security; điều cần nhớ ở đây với developer là việc sinh SBOM nên là một bước tự động trong build pipeline, không phải một tài liệu duy trì thủ công.

Secure code review & SAST tổng quan

Secure code review là một lượt review của con người, tập trung riêng vào các thay đổi liên quan đến bảo mật, khác biệt (nhưng bổ trợ) cho một lượt review đúng đắn/style chung. Nó hiệu quả nhất khi reviewer biết chính xác cần tìm gì:

Static Application Security Testing (SAST) là công cụ phân tích source code (không thực thi nó) để gắn cờ các pattern khớp với các lớp lỗ hổng đã biết — SQL ghép chuỗi, dùng eval, hardcode secrets, tắt xác thực certificate, và tương tự. SAST bổ trợ cho code review (nó bắt các pattern mang tính cơ học ở quy mô lớn và ở mọi commit; reviewer con người bắt các vấn đề business logic và thiết kế mà SAST không thể suy luận) và bổ trợ cho SCA (SAST nhìn vào code của bạn; SCA nhìn vào code của dependency).

Công cụTrọng tâm ngôn ngữGhi chú
SemgrepĐa ngôn ngữNhanh, dựa trên rule, dễ viết rule tùy chỉnh, phổ biến trong CI
SonarQube / SonarCloudĐa ngôn ngữKết hợp SAST với các chỉ số chất lượng code
CodeQLĐa ngôn ngữGitHub-native, phân tích ngữ nghĩa/dựa trên query, vận hành GitHub code scanning
BanditPythonNhẹ, quét pattern chuyên biệt cho Python
ESLint security pluginsJavaScript/TypeScriptPhát hiện các pattern không an toàn phổ biến tại thời điểm lint

Kết quả SAST vốn nhiễu (false positive phổ biến) — pattern thực tế là chạy nó ở mọi PR, phân loại (triage) kết quả theo mức độ nghiêm trọng, và chỉ chặn merge với các lớp có độ tin cậy cao, mức nghiêm trọng cao (hardcoded secrets, pattern SQL injection), trong khi đưa các kết quả có độ tin cậy thấp hơn vào hàng đợi review thay vì một cổng chặn cứng, để tránh alert fatigue khiến team lờ đi công cụ hoàn toàn.

Best Practices

Tài liệu tham khảo

Part of the DevSecOps Roadmap knowledge base.

Overview

Secure coding is the practice of writing software that resists misuse by design, not by luck. Most breaches do not start with an exotic zero-day — they start with a query built from string concatenation, a template that renders attacker-controlled HTML unescaped, a missing authorization check on an endpoint, or a dependency with a known CVE that nobody updated. These are ordinary coding mistakes, and every one of them is preventable with well-understood, mechanical defenses: parameterized queries, output encoding, allowlisting, and least privilege enforced at every layer.

This note is defensive only. For every vulnerability class we describe the failure mode just deeply enough to recognize it, then show the concrete control that neutralizes it — safe code next to unsafe code, every time. The goal is a developer who can look at a diff and immediately spot “this concatenates user input into a query” or “this renders raw HTML,” and knows the one-line fix.

DevSecOps treats this as everyone’s job, not a gate at the end. Secure coding practices, automated scanning (SAST for code, SCA for dependencies), and code review are the controls that run inside the development loop — shifting security left so that vulnerabilities are caught while they are cheap to fix, not after they reach production. This note focuses on the application-code layer; pipeline-level scanning and supply-chain controls are covered in CI/CD & Supply Chain Security, and how to prioritize which risks matter most for your system is covered in Threat Modeling & Risk Assessment.

Fundamentals

Core secure coding principles

A handful of principles generate most of the specific rules you will ever apply. Internalize these and you can derive the correct defense for a vulnerability class you have never seen before.

PrincipleWhat it meansExample application
Never trust inputAny data crossing a trust boundary (user input, API request, file upload, third-party response, environment variable) is hostile until proven otherwiseValidate and encode all request parameters, headers, cookies, and even data read back from your own database if it can be poisoned by another user
Fail securelyWhen something goes wrong, fail closed (deny) — not open (allow)An authorization check that throws an exception must deny access, not silently permit the request through
Least privilegeEvery component, user, and process gets the minimum access needed and nothing moreA reporting service account gets SELECT only, never DROP or DELETE; a Lambda function’s IAM role is scoped to the one bucket it needs
Secure defaultsThe out-of-the-box configuration is the safe one; insecure behavior must be opted into explicitlyNew API endpoints require authentication unless explicitly marked public; cookies default to Secure, HttpOnly, SameSite=Lax
Defense in depthStack multiple independent controls so that one failure does not equal a breachInput validation and parameterized queries and a least-privilege DB user and a WAF — any one alone is not “the” defense
Minimize attack surfaceEvery endpoint, feature flag, and exposed field is something that can be attacked — remove what isn’t neededDisable directory listing, remove debug endpoints before shipping, don’t expose internal IDs or stack traces
Complete mediationCheck authorization on every access, every time — never cache “already checked” across requests or objectsRe-verify ownership of order_id=123 on every request, even if the user was authorized for a different order a minute ago (this is what prevents IDOR)
Separation of dutiesNo single actor (human or service) should be able to both make and approve a critical change uncheckedCode review required before merge; the CI pipeline that deploys is not the same identity that can approve a PR

These are not abstract slogans — each one maps directly to a concrete code-level control shown later in this note.

The OWASP Top 10 (2021)

The OWASP Top 10 is the industry-standard shortlist of the web application risks most likely to cause real breaches. It is a prioritization tool, not an exhaustive checklist — treat it as the minimum bar.

#CategoryOne-line descriptionPrimary defense
A01Broken Access ControlUsers can act outside their intended permissions (view/edit others’ data, escalate privilege)Deny by default; enforce authorization server-side on every request; centralize the check, don’t scatter it
A02Cryptographic FailuresSensitive data exposed due to weak, missing, or misused cryptography (plaintext storage, weak ciphers, no TLS)Encrypt data in transit (TLS 1.2+) and at rest; use vetted algorithms and libraries, never roll your own crypto
A03InjectionUntrusted data sent to an interpreter (SQL, OS shell, LDAP, NoSQL) is executed as code/commandsParameterized queries / prepared statements; escape or avoid interpreters entirely; validate input
A04Insecure DesignMissing or ineffective security controls baked into the architecture itself, not just the implementationThreat modeling during design; secure design patterns; abuse-case testing
A05Security MisconfigurationDefault credentials, verbose error messages, unnecessary features enabled, missing security headersHardened, automated configuration baselines; disable what you don’t use; secure defaults
A06Vulnerable and Outdated ComponentsUsing libraries/frameworks with known vulnerabilities, or unsupported/EOL versionsSCA scanning in CI; patch cadence; remove unused dependencies
A07Identification and Authentication FailuresWeak session management, credential stuffing, missing MFA, predictable session tokensStrong session handling, MFA, rate-limited login, secure password hashing (bcrypt/argon2)
A08Software and Data Integrity FailuresTrusting unsigned/unverified code, plugins, or CI/CD pipelines (insecure deserialization included here)Verify signatures/checksums, use SBOMs, lock dependency versions, protect the CI/CD pipeline
A09Security Logging and Monitoring FailuresBreaches go undetected because nothing logs, alerts, or is monitoredCentralized logging of auth events and access-control failures; alerting; retained audit trail
A10Server-Side Request Forgery (SSRF)The server is tricked into making a request to an unintended (often internal) destinationAllowlist outbound destinations; disable unused URL schemes/redirects; network-segment internal services

The list changes over time (the OWASP Top 10 is refreshed roughly every 3-4 years) — always confirm you’re referencing the current edition.

Key Concepts

Injection prevention: SQL injection

SQL injection happens when untrusted input is concatenated directly into a query string, letting an attacker alter the query’s logic. The fix is structural, not cosmetic: never build a query by string-concatenating user input. Use parameterized queries (prepared statements) so the database always treats input as data, never as code, regardless of what characters it contains.

# UNSAFE — string concatenation lets input alter query structure
def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    cursor.execute(query)
    return cursor.fetchone()

# SAFE — parameterized query; the driver sends input as a bound value,
# never as part of the SQL text
def get_user(username):
    query = "SELECT * FROM users WHERE username = %s"
    cursor.execute(query, (username,))
    return cursor.fetchone()
// UNSAFE — Statement executes whatever string you hand it
String query = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);

// SAFE — PreparedStatement binds parameters out-of-band from the SQL text
String query = "SELECT * FROM users WHERE username = ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();

ORM safety is not automatic. ORMs (SQLAlchemy, Hibernate, ActiveRecord, Sequelize, Prisma) parameterize queries built through their query API by default — but every ORM offers an escape hatch for raw SQL, and that escape hatch is exactly as unsafe as hand-written SQL if you concatenate into it.

// UNSAFE — raw query escape hatch, string-interpolated
await sequelize.query(`SELECT * FROM users WHERE email = '${email}'`);

// SAFE — same raw query escape hatch, but parameterized
await sequelize.query(
  "SELECT * FROM users WHERE email = :email",
  { replacements: { email }, type: QueryTypes.SELECT }
);

// SAFEST — use the query builder / ORM API, which parameterizes by construction
await User.findOne({ where: { email } });

Additional layers of defense: validate input shape before it even reaches the query (an email field should look like an email), and run the query under a least-privilege database account that cannot DROP TABLE or read tables it has no business touching — so that even a successful injection is contained.

Injection isn’t only SQL: the same “never concatenate untrusted input into an interpreter” rule applies to OS command execution (use argument arrays / APIs like Python’s subprocess.run([...]), never shell=True with concatenated input), LDAP queries, NoSQL query objects (a raw $where clause in MongoDB is just as injectable as a SQL string), and template engines.

Cross-Site Scripting (XSS) prevention

XSS happens when untrusted data is rendered into a page as if it were trusted HTML/JavaScript, letting an attacker’s script execute in another user’s browser session. There are three flavors: stored (payload saved server-side, e.g. in a comment, and served to later visitors), reflected (payload bounced back immediately in a response, e.g. a search results page), and DOM-based (the vulnerability lives entirely in client-side JavaScript that writes untrusted data into the DOM, without the payload ever necessarily touching the server).

The primary defense is output encoding: encode data for the context it is being rendered into (HTML body, HTML attribute, JavaScript string, URL) at the point of output, not at the point of input.

// UNSAFE — raw HTML injection via innerHTML
element.innerHTML = "Welcome, " + userSuppliedName;

// SAFE — textContent never interprets its argument as markup
element.textContent = "Welcome, " + userSuppliedName;
// UNSAFE (React) — dangerouslySetInnerHTML does exactly what it says
function Comment({ text }) {
  return <div dangerouslySetInnerHTML={{ __html: text }} />;
}

// SAFE (React) — JSX auto-escapes interpolated values by default
function Comment({ text }) {
  return <div>{text}</div>;
}
<!-- UNSAFE (server-rendered template) — raw interpolation -->
<p>Search results for: {{{ query }}}</p>  <!-- triple braces = unescaped in many engines -->

<!-- SAFE — default (auto-escaping) interpolation -->
<p>Search results for: {{ query }}</p>

Modern templating frameworks auto-escape by default — Jinja2, Django templates, React JSX, Vue, Angular, and Rails ERB (with <%= %>) all HTML-escape interpolated values unless you explicitly opt out (|safe, dangerouslySetInnerHTML, v-html, {!! !!}). The vulnerability is almost always introduced by explicitly bypassing the framework’s default, so treat every use of a “raw/unescaped” helper as a place that needs manual review and justification.

DOM-based XSS deserves special attention because it can occur with zero server involvement: client-side JavaScript reads from an untrusted source (location.hash, document.referrer, postMessage data, URL parameters) and writes it into a dangerous sink (innerHTML, document.write, eval, setTimeout with a string argument, element.setAttribute("href", ...) with a javascript: URL). The fix is the same principle applied client-side: never route untrusted sources into dangerous sinks without encoding, and prefer safe sinks (textContent, setAttribute for non-URL attributes, DOM APIs like createElement/appendChild over string-based HTML construction).

// UNSAFE — DOM-based XSS: location.hash (attacker-controlled) into innerHTML
document.getElementById("greeting").innerHTML = decodeURIComponent(location.hash.slice(1));

// SAFE — treat it as text, not markup
document.getElementById("greeting").textContent = decodeURIComponent(location.hash.slice(1));

Content-Security-Policy (CSP) is a second, independent layer of defense: an HTTP response header that restricts which sources of scripts, styles, and other resources the browser is allowed to load and execute, so that even if an injection slips through, an attacker’s inline <script> or externally hosted payload simply won’t run.

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'self'

Avoid unsafe-inline and unsafe-eval in script-src wherever possible — they neutralize much of CSP’s protection against XSS. Prefer a nonce- or hash-based allowlist for the rare inline script you do need:

Content-Security-Policy: script-src 'self' 'nonce-r4nd0mBase64PerRequest'

Input validation patterns

Input validation is a trust-boundary control: it belongs at every point where data crosses from a less-trusted context into a more-trusted one — the edge of your API, before a value is used in a query, before it’s written to a file path, before it’s passed to a downstream service. Validating once at the API gateway and then trusting the value everywhere downstream is a common mistake — internal services and background jobs get called from more places than the original entry point over a system’s lifetime.

Allowlisting (positive validation) vs. denylisting (negative validation):

ApproachHow it worksReliability
AllowlistDefine exactly what is permitted (a pattern, an enum, a set of known-good values) and reject everything elseStrong — the set of “bad” inputs is infinite and unenumerable, but the set of “good” inputs is usually small and known
DenylistDefine known-bad patterns (e.g., block the string <script>) and reject matchesWeak — attackers routinely find encodings/variants (<ScRiPt>, HTML entities, double-encoding) that slip past a denylist; treat this as a defense-in-depth extra, never the primary control

Prefer allowlisting whenever the valid input space is enumerable: a country code is one of ~250 values, a “sort order” parameter is asc or desc, a file extension for uploads is one of a handful you explicitly support.

# UNSAFE — denylist tries to catch "bad" characters, easy to bypass
def is_safe_filename(name):
    return "../" not in name and ".." not in name  # misses many traversal encodings

# SAFE — allowlist: only accept a strict, known-good pattern
import re
def is_safe_filename(name):
    return bool(re.fullmatch(r"[a-zA-Z0-9_\-]{1,64}\.(png|jpg|pdf)", name))

Schema validation formalizes allowlisting for structured input (JSON bodies, query strings): define the exact shape, types, ranges, and required fields you accept, and reject anything that doesn’t conform — before any business logic runs.

// Node.js example using a schema validation library (e.g., zod / joi / ajv)
const schema = z.object({
  email: z.string().email(),
  age: z.number().int().min(0).max(120),
  role: z.enum(["viewer", "editor", "admin"]),
});

app.post("/users", (req, res) => {
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ error: "Invalid input" });
  }
  // result.data is now validated and typed — safe to use downstream
});

Validation is necessary but not sufficient by itself — it reduces the input space, but output encoding and parameterized queries are still required at every sink, because “valid” input (e.g., a legitimately apostrophe-containing name like O'Brien) can still be dangerous if the sink isn’t defended independently. This is defense in depth: validate at the boundary, encode/parameterize at the sink.

Secure API design

APIs deserve dedicated attention because they are frequently the most exposed surface of a system and the most common place authorization checks get skipped. Deeper coverage of API design and web-security mechanics lives in Backend — APIs and Backend — Web Security; the DevSecOps-relevant summary:

// UNSAFE — Mass Assignment: whatever the client sends gets applied to the model
app.put("/api/users/:id", async (req, res) => {
  await User.update(req.body, { where: { id: req.params.id } });
});

// SAFE — explicit allowlist of updatable fields; ownership re-checked per request
app.put("/api/users/:id", requireAuth, async (req, res) => {
  if (req.user.id !== req.params.id && !req.user.isAdmin) {
    return res.status(403).json({ error: "Forbidden" });
  }
  const { displayName, bio } = req.body; // only these fields are ever accepted
  await User.update({ displayName, bio }, { where: { id: req.params.id } });
});

Dependency risk management

Modern applications are assembled from dozens to thousands of open-source dependencies (direct and transitive), and OWASP’s A06: Vulnerable and Outdated Components exists precisely because this attack surface is large and frequently unmonitored. A vulnerability disclosed in a widely used library (e.g., a deserialization flaw or a path traversal bug in a logging framework) instantly becomes a vulnerability in every application that bundles the affected version.

Software Composition Analysis (SCA) tooling scans your dependency manifests (and often your built artifacts) against vulnerability databases (the NVD, GitHub Advisory Database, OSV) and flags known-CVE dependencies, license issues, and outdated versions.

ToolEcosystemNotes
npm audit / yarn auditNode.jsBuilt into the package manager; free baseline
pip-auditPythonScans installed packages / requirements files against the PyPA advisory DB
DependabotGitHub-native, multi-ecosystemOpens automated PRs to bump vulnerable dependency versions
SnykMulti-ecosystemSCA + container + IaC scanning, commercial with a free tier
OWASP Dependency-CheckJava, .NET, othersOpen-source, CI-friendly, matches against the NVD
TrivyContainers, IaC, filesystems, SCAOpen-source, widely used in CI pipelines

Practical hygiene beyond scanning: pin dependency versions (lockfiles), remove unused dependencies (every unused dependency is attack surface with zero benefit), prefer well-maintained libraries with active security response, and treat a dependency update as a routine, low-risk operation by keeping the gap between “vulnerability disclosed” and “you upgrade” as small as possible — automate it (Dependabot/Renovate) rather than doing it manually on a quarterly cadence.

Supply chain security & SBOMs (brief)

A Software Bill of Materials (SBOM) is a formal, machine-readable inventory of every component (direct and transitive dependency, its version, and its license) that makes up a piece of software — conceptually the same idea as an ingredients list on packaged food. It matters because you cannot assess or respond to risk in components you don’t know you have: when a new CVE drops for a library, an SBOM lets you answer “are we affected, and where” in minutes via a query instead of days of manual auditing across every service.

The two dominant formats are SPDX (Software Package Data Exchange, an ISO standard, originally licensing-focused) and CycloneDX (OWASP-maintained, security-focused, with native vulnerability and provenance extensions) — both are JSON/XML-based and tool-generatable at build time. SBOM generation, signing, and how it fits into a broader supply-chain security posture (build provenance, artifact signing, SLSA levels) is covered in depth in CI/CD & Supply Chain Security; the developer-facing takeaway here is that SBOM generation should be an automated build-pipeline step, not a manual document maintained by hand.

Secure code review & SAST at a glance

Secure code review is a human review pass focused specifically on security-relevant changes, distinct from (and complementary to) a general correctness/style review. It’s most effective when reviewers know what to look for:

Static Application Security Testing (SAST) tools analyze source code (without executing it) to flag patterns that match known vulnerability classes — string-concatenated SQL, use of eval, hardcoded secrets, disabled certificate validation, and similar. SAST is complementary to code review (it catches mechanical patterns at scale and on every commit; a human reviewer catches business-logic and design-level issues SAST cannot reason about) and complementary to SCA (SAST looks at your code; SCA looks at dependency code).

ToolLanguage focusNotes
SemgrepMulti-languageFast, rule-based, easy to write custom rules, popular in CI
SonarQube / SonarCloudMulti-languageCombines SAST with code quality metrics
CodeQLMulti-languageGitHub-native, semantic/query-based analysis, powers GitHub code scanning
BanditPythonLightweight, Python-specific pattern scanner
ESLint security pluginsJavaScript/TypeScriptLint-time detection of common unsafe patterns

SAST findings are noisy by nature (false positives are common) — the practical pattern is to run it on every PR, triage findings by severity, and block merges only on high-confidence, high-severity classes (hardcoded secrets, SQL injection patterns) while routing lower-confidence findings to a review queue rather than a hard gate, to avoid alert fatigue that causes teams to ignore the tool entirely.

Best Practices

References