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ĩa | Ví 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ại | Validate 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ơn | Tà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 minh | API 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ụ breach | Input validation và parameterized queries và tài khoản DB least-privilege và WAF — không lớp nào một mình là “biện pháp duy nhất” |
| Giảm thiểu attack surface | Mỗ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ết | Tắ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 nhau | Xá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át | Yê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.
| # | Category | Mô tả ngắn gọn | Biện pháp phòng thủ chính |
|---|---|---|---|
| A01 | Broken Access Control | User 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 |
| A02 | Cryptographic Failures | Dữ 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 |
| A03 | Injection | Dữ 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ệnh | Parameterized queries / prepared statements; escape hoặc tránh hoàn toàn interpreter; validate input |
| A04 | Insecure Design | Thiế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 khai | Threat modeling trong giai đoạn thiết kế; secure design pattern; abuse-case testing |
| A05 | Security Misconfiguration | Credential 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 header | Chuẩn cấu hình đã hardening và tự động hóa; tắt những gì không dùng; secure defaults |
| A06 | Vulnerable and Outdated Components | Sử 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ợ/EOL | Quét SCA trong CI; duy trì tần suất patch; loại bỏ dependency không dùng |
| A07 | Identification and Authentication Failures | Quản lý session yếu, credential stuffing, thiếu MFA, session token dễ đoán | Quả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) |
| A08 | Software and Data Integrity Failures | Tin 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 |
| A09 | Security Logging and Monitoring Failures | Vi 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át | Log tập trung các sự kiện auth và lỗi access-control; alerting; lưu trữ audit trail |
| A10 | Server-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-inline và unsafe-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ận | Cơ 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ác | Mạ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ớp | Yế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 — APIs và Backend — Web Security; tóm tắt liên quan tới DevSecOps:
- Authentication và authorization ở mọi endpoint — coi “không có kiểm tra auth” là một bug, không phải một mặc định. Thực thi access control phía server, ở mọi request, kiểm tra lại quyền sở hữu ở cấp object (đây chính là điều ngăn chặn Broken Object Level Authorization / IDOR — một user đổi
/api/orders/123thành/api/orders/124và truy cập được dữ liệu của người khác). Không bao giờ dựa vào một URL ẩn, một kiểm tra phía client, hay “user sẽ không đoán được ID đó” như một biện pháp kiểm soát. - Rate limiting và throttling — bảo vệ trước credential stuffing, brute force, và lạm dụng làm cạn kiệt tài nguyên ở tầng API (giới hạn theo IP, theo tài khoản, theo token), không chỉ ở rìa mạng.
- Tránh over-exposure dữ liệu — không serialize toàn bộ object database nội bộ trả về cho client “cho tiện”. Định nghĩa DTO/schema response tường minh cho từng endpoint để việc thêm một trường nội bộ vào model không âm thầm bị lộ ra ngoài (đây chính là pattern Excessive Data Exposure của OWASP, có liên hệ chặt chẽ với Mass Assignment quá lỏng lẻo, khi chấp nhận toàn bộ object client gửi lên để update cho phép attacker set các trường như
isAdminvốn không bao giờ được thiết kế để client ghi được). - Secure defaults — trường mới, endpoint mới, và role mới nên mặc định ở mức truy cập tối thiểu, yêu cầu một quyết định tường minh để mở rộng, không phải ngược lại.
- Pagination và giới hạn resource — giới hạn kích thước trang và độ phức tạp query (đặc biệt trong GraphQL) để tránh một request đơn lẻ làm cạn kiệt tài nguyên server.
// 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ụ | Ecosystem | Ghi chú |
|---|---|---|
npm audit / yarn audit | Node.js | Tích hợp sẵn trong package manager; baseline miễn phí |
pip-audit | Python | Quét package đã cài / requirements file dựa trên PyPA advisory DB |
| Dependabot | GitHub-native, đa ecosystem | Tự động mở PR để nâng cấp phiên bản dependency có lỗ hổng |
| Snyk | Đa ecosystem | SCA + quét container + IaC, thương mại có gói miễn phí |
| OWASP Dependency-Check | Java, .NET, và khác | Mã nguồn mở, thân thiện với CI, đối chiếu với NVD |
| Trivy | Container, IaC, filesystem, SCA | Mã 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ì:
- Bất kỳ thay đổi nào chạm vào authentication, authorization, quản lý session, hoặc cryptography.
- Bất kỳ raw SQL mới, render raw HTML mới, xây dựng đường dẫn file, hoặc thực thi lệnh bên ngoài.
- Bất kỳ endpoint public-facing mới nào — nó có kiểm tra auth không, và kiểm tra đó có thể test/review được trong diff không?
- Bất kỳ thay đổi phiên bản dependency nào, đặc biệt trong lockfile, mà không đi kèm giải thích.
- Secrets: có gì trông giống credential, key, hay token đang được thêm vào diff không?
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 |
| Bandit | Python | Nhẹ, quét pattern chuyên biệt cho Python |
| ESLint security plugins | JavaScript/TypeScript | Phá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
- Validate ở mọi trust boundary, encode/parameterize ở mọi sink — đây là hai biện pháp kiểm soát riêng biệt, bổ trợ nhau; đừng coi input validation là thứ thay thế cho output encoding hay parameterized query.
- Ưu tiên allowlisting hơn denylisting bất cứ khi nào không gian input hợp lệ có thể liệt kê được — nó xuống cấp một cách “graceful” trước các input bạn chưa lường tới, denylist thì không.
- Không bao giờ xây dựng chuỗi có thể diễn giải (SQL, lệnh shell, LDAP filter, template) bằng cách ghép chuỗi input không tin cậy — chỉ dùng API parameterized, argument array, hoặc builder API an toàn.
- Để framework tự động escape — audit và giải trình mọi lần dùng cửa hậu “raw/unsafe/dangerouslySet”; coi đó là một mục cần review đặc biệt, không phải code thường quy.
- Thực thi authorization phía server, ở mỗi request, ở mỗi object — kiểm tra phía client, endpoint ẩn, và ID “khó đoán” không phải là access control.
- Áp dụng least privilege cho tài khoản database, service account, và API token — giới hạn mỗi cái chỉ có đúng những gì nó cần, để một exploit thành công ở một nơi không lan rộng ra toàn hệ thống.
- Tự động hóa quét dependency (SCA) và giữ độ trễ patch nhỏ — một CVE đã biết nằm không được vá trong nhiều tháng là một lỗ hổng tự gây ra.
- Sinh SBOM như một phần của quá trình build, không phải như một việc làm sau cùng — bạn không thể ứng phó nhanh với một công bố về supply chain nếu không có nó.
- Chạy SAST ở mọi PR nhưng điều chỉnh nó để tránh nhiễu quá mức — chặn cứng với các phát hiện có độ tin cậy cao, đưa phần còn lại vào review.
- Coi secure code review là một lượt checklist riêng biệt, không phải hy vọng rằng review thông thường sẽ tình cờ nhận ra vấn đề bảo mật.
- Đặt secure defaults ở mọi nơi — endpoint mới mặc định yêu cầu auth, cookie mới mặc định
Secure/HttpOnly/SameSite, IAM role mới bắt đầu với zero quyền và được cấp thêm dần, không phải ngược lại. - Triển khai Content-Security-Policy và tránh
unsafe-inline/unsafe-eval— coi CSP là lớp phòng thủ thứ hai độc lập đứng sau output encoding, không phải thứ thay thế cho nó. - Log và cảnh báo các lỗi authorization — một đợt tăng đột biến 403 nhắm vào các object ID tuần tự là một tín hiệu, không phải nhiễu, và ánh xạ trực tiếp tới OWASP A09.
Tài liệu tham khảo
- OWASP Top 10 (2021)
- OWASP Cheat Sheet Series
- OWASP SQL Injection Prevention Cheat Sheet
- OWASP Cross Site Scripting (XSS) Prevention Cheat Sheet
- OWASP Input Validation Cheat Sheet
- OWASP API Security Top 10
- OWASP Content Security Policy Cheat Sheet
- CycloneDX Specification / SPDX Specification
- Các note liên quan: Backend — APIs, Backend — Web Security, Threat Modeling & Risk Assessment, CI/CD & Supply Chain Security
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.
| Principle | What it means | Example application |
|---|---|---|
| Never trust input | Any data crossing a trust boundary (user input, API request, file upload, third-party response, environment variable) is hostile until proven otherwise | Validate 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 securely | When 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 privilege | Every component, user, and process gets the minimum access needed and nothing more | A 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 defaults | The out-of-the-box configuration is the safe one; insecure behavior must be opted into explicitly | New API endpoints require authentication unless explicitly marked public; cookies default to Secure, HttpOnly, SameSite=Lax |
| Defense in depth | Stack multiple independent controls so that one failure does not equal a breach | Input validation and parameterized queries and a least-privilege DB user and a WAF — any one alone is not “the” defense |
| Minimize attack surface | Every endpoint, feature flag, and exposed field is something that can be attacked — remove what isn’t needed | Disable directory listing, remove debug endpoints before shipping, don’t expose internal IDs or stack traces |
| Complete mediation | Check authorization on every access, every time — never cache “already checked” across requests or objects | Re-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 duties | No single actor (human or service) should be able to both make and approve a critical change unchecked | Code 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.
| # | Category | One-line description | Primary defense |
|---|---|---|---|
| A01 | Broken Access Control | Users 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 |
| A02 | Cryptographic Failures | Sensitive 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 |
| A03 | Injection | Untrusted data sent to an interpreter (SQL, OS shell, LDAP, NoSQL) is executed as code/commands | Parameterized queries / prepared statements; escape or avoid interpreters entirely; validate input |
| A04 | Insecure Design | Missing or ineffective security controls baked into the architecture itself, not just the implementation | Threat modeling during design; secure design patterns; abuse-case testing |
| A05 | Security Misconfiguration | Default credentials, verbose error messages, unnecessary features enabled, missing security headers | Hardened, automated configuration baselines; disable what you don’t use; secure defaults |
| A06 | Vulnerable and Outdated Components | Using libraries/frameworks with known vulnerabilities, or unsupported/EOL versions | SCA scanning in CI; patch cadence; remove unused dependencies |
| A07 | Identification and Authentication Failures | Weak session management, credential stuffing, missing MFA, predictable session tokens | Strong session handling, MFA, rate-limited login, secure password hashing (bcrypt/argon2) |
| A08 | Software and Data Integrity Failures | Trusting 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 |
| A09 | Security Logging and Monitoring Failures | Breaches go undetected because nothing logs, alerts, or is monitored | Centralized logging of auth events and access-control failures; alerting; retained audit trail |
| A10 | Server-Side Request Forgery (SSRF) | The server is tricked into making a request to an unintended (often internal) destination | Allowlist 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):
| Approach | How it works | Reliability |
|---|---|---|
| Allowlist | Define exactly what is permitted (a pattern, an enum, a set of known-good values) and reject everything else | Strong — the set of “bad” inputs is infinite and unenumerable, but the set of “good” inputs is usually small and known |
| Denylist | Define known-bad patterns (e.g., block the string <script>) and reject matches | Weak — 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:
- Authentication and authorization on every endpoint — treat “no auth check” as a bug, not a default. Enforce access control server-side, on every request, re-checking object-level ownership (this is what prevents Broken Object Level Authorization / IDOR — a user changing
/api/orders/123to/api/orders/124and reaching someone else’s data). Never rely on a hidden URL, a client-side check, or “the user wouldn’t guess that ID” as a control. - Rate limiting and throttling — protect against credential stuffing, brute force, and resource-exhaustion abuse at the API layer (per-IP, per-account, and per-token limits), not just at the network edge.
- Avoid over-exposure of data — don’t serialize entire internal database objects back to the client “because it’s convenient.” Define explicit response DTOs/schemas per endpoint so that adding an internal field to a model doesn’t silently leak it (this is OWASP’s Excessive Data Exposure pattern, closely related to over-permissive Mass Assignment, where accepting a whole client-supplied object for an update lets an attacker set fields like
isAdminthat were never meant to be client-writable). - Secure defaults — new fields, new endpoints, and new roles should default to the least access, requiring an explicit decision to widen them, not the reverse.
- Pagination and resource limits — cap page sizes and query complexity (especially in GraphQL) to prevent a single request from exhausting server resources.
// 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.
| Tool | Ecosystem | Notes |
|---|---|---|
npm audit / yarn audit | Node.js | Built into the package manager; free baseline |
pip-audit | Python | Scans installed packages / requirements files against the PyPA advisory DB |
| Dependabot | GitHub-native, multi-ecosystem | Opens automated PRs to bump vulnerable dependency versions |
| Snyk | Multi-ecosystem | SCA + container + IaC scanning, commercial with a free tier |
| OWASP Dependency-Check | Java, .NET, others | Open-source, CI-friendly, matches against the NVD |
| Trivy | Containers, IaC, filesystems, SCA | Open-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:
- Any change touching authentication, authorization, session handling, or cryptography.
- Any new raw SQL, raw HTML rendering, file path construction, or external command execution.
- Any new external-facing endpoint — does it have an auth check, and is that check testable/reviewable in the diff?
- Any change to dependency versions, especially in a lockfile, that isn’t paired with an explanation.
- Secrets: is anything that looks like a credential, key, or token being added to the diff?
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).
| Tool | Language focus | Notes |
|---|---|---|
| Semgrep | Multi-language | Fast, rule-based, easy to write custom rules, popular in CI |
| SonarQube / SonarCloud | Multi-language | Combines SAST with code quality metrics |
| CodeQL | Multi-language | GitHub-native, semantic/query-based analysis, powers GitHub code scanning |
| Bandit | Python | Lightweight, Python-specific pattern scanner |
| ESLint security plugins | JavaScript/TypeScript | Lint-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
- Validate at every trust boundary, encode/parameterize at every sink — these are two separate, complementary controls; don’t treat input validation as a substitute for output encoding or parameterized queries.
- Prefer allowlisting over denylisting whenever the valid input space is enumerable — it degrades gracefully against inputs you didn’t anticipate, denylists do not.
- Never build interpretable strings (SQL, shell commands, LDAP filters, templates) via concatenation of untrusted input — use parameterized APIs, argument arrays, or safe builder APIs exclusively.
- Let your framework auto-escape — audit and justify every use of a “raw/unsafe/dangerouslySet” escape hatch; treat it as a flagged review item, not routine code.
- Enforce authorization server-side, per request, per object — client-side checks, hidden endpoints, and “unguessable” IDs are not access control.
- Apply least privilege to database accounts, service accounts, and API tokens — scope each to only what it needs, so a successful exploit in one place doesn’t cascade.
- Automate dependency scanning (SCA) and keep the patch lag small — a known CVE sitting unpatched for months is a self-inflicted vulnerability.
- Generate SBOMs as part of the build, not as an afterthought — you cannot respond quickly to a supply-chain disclosure without one.
- Run SAST on every PR but tune it to avoid noise fatigue — gate hard on high-confidence findings, route the rest to review.
- Treat security code review as a distinct checklist pass, not a hope that general review will happen to notice security issues.
- Set secure defaults everywhere — new endpoints require auth by default, new cookies are
Secure/HttpOnly/SameSiteby default, new IAM roles start with zero permissions and are granted up, not down. - Ship a Content-Security-Policy and avoid
unsafe-inline/unsafe-eval— treat CSP as a second independent layer behind output encoding, not a replacement for it. - Log and alert on authorization failures — a spike in 403s against sequential object IDs is a signal, not noise, and maps directly to OWASP A09.
References
- OWASP Top 10 (2021)
- OWASP Cheat Sheet Series
- OWASP SQL Injection Prevention Cheat Sheet
- OWASP Cross Site Scripting (XSS) Prevention Cheat Sheet
- OWASP Input Validation Cheat Sheet
- OWASP API Security Top 10
- OWASP Content Security Policy Cheat Sheet
- CycloneDX Specification / SPDX Specification
- Related notes: Backend — APIs, Backend — Web Security, Threat Modeling & Risk Assessment, CI/CD & Supply Chain Security