Viết Custom Rules Semgrep: Tự động hóa Audit trong CI/CD
Làm chủ cú pháp Semgrep YAML: Khai báo pattern, pattern-inside, metavariable-regex, cấu hình taint-mode để tự động phát hiện lỗ hổng ứng dụng.
TL;DR: Semgrep đã định nghĩa lại cách thức các kỹ sư bảo mật viết quy tắc quét mã nguồn tĩnh (SAST). Thay vì phải học các ngôn ngữ truy vấn phức tạp như CodeQL hay vật lộn với các regex sai số cao, Semgrep cho phép bạn viết quy tắc bằng chính cú pháp mã nguồn của ngôn ngữ đích, kết hợp với các biến siêu hình (Metavariables như $X, $QUERY) và toán tử ellipsis (...). Bài học này hướng dẫn bạn cách viết các rule Semgrep tùy biến, cấu hình Taint Mode đa tầng (Source $\to$ Sink) để tự động bắt các lỗi bảo mật nghiệp vụ trong pipeline CI/CD của doanh nghiệp.
1. Scenario — Chặn đứng 3 lỗ hổng SQLi liên tiếp trong 1 sprint
Một công ty tài chính công nghệ vận hành hơn 40 microservices viết bằng Go. Trong một quý, đội ngũ pentest liên tiếp phát hiện 3 lỗ hổng SQL Injection do các lập trình viên mới sử dụng fmt.Sprintf để ghép chuỗi vào mệnh đề ORDER BY (như đã học ở Bài White-Box Audit Go & Node.js).
Sau mỗi lần phát hiện, Security Lead tổ chức họp nhắc nhở và viết tài liệu hướng dẫn nội bộ. Tuy nhiên, ở sprint tiếp theo, một lập trình viên khác lại tiếp tục commit một đoạn code tương tự:
query := fmt.Sprintf("SELECT * FROM transactions WHERE account_id = $1 ORDER BY " + sortField)
db.Query(ctx, query, accountId)
Nhận thấy việc nhắc nhở bằng miệng không thể giải quyết được vấn đề ở quy mô lớn, Security Lead quyết định tự động hóa khâu kiểm soát: viết một quy tắc Semgrep tùy biến dài đúng 12 dòng và tích hợp vào GitHub Actions. Kể từ đó, bất kỳ pull request nào chứa lệnh ghép chuỗi vào hàm truy vấn cơ sở dữ liệu đều bị chặn đứng ngay lập tức với cảnh báo đỏ kèm link tài liệu sửa lỗi. Không còn bất kỳ lỗ hổng SQLi nào lọt được lên môi trường production.
2. Analogy — Máy quét an ninh sân bay và quy tắc nhận diện vũ khí
Hãy hình dung quy trình kiểm soát an ninh tại sân bay:
- Cách tiếp cận Regex truyền thống: Nhân viên an ninh cầm một danh sách chữ viết: "Chặn từ 'dao', chặn từ 'súng'". Nếu hành khách bọc con dao trong một chiếc hộp ghi nhãn "Dụng cụ gọt hoa quả", danh sách từ ngữ hoàn toàn vô hiệu.
- Cách tiếp cận Semgrep (AST Pattern Matching): Chiếc máy quét tia X soi thẳng vào cấu trúc hình học bên trong vali. Dù hành khách đặt tên món đồ là gì hay gói nó bằng bao nhiêu lớp giấy, máy quét vẫn nhận diện được hình dáng kim loại sắc nhọn của lưỡi dao và phát chuông cảnh báo.
Semgrep không đọc code như những chuỗi văn bản thô vô tri. Nó phân tích cây cú pháp trừu tượng (AST), hiểu rõ đâu là lời gọi hàm, đâu là đối số, và đâu là luồng dữ liệu ô nhiễm.
3. Bản chất của Semgrep AST Matching

Cú pháp của một rule Semgrep được viết bằng định dạng YAML với hai khối xây dựng nền tảng:
3.1. Metavariables (Biến siêu hình: $X, $QUERY, $ARG)
Bắt đầu bằng ký tự $, metavariables đại diện cho một biểu thức, một biến hoặc một hàm bất kỳ. Khi một metavariable xuất hiện nhiều lần trong cùng một pattern, Semgrep bắt buộc giá trị của chúng phải trùng khớp (Equality enforcement):
# Bat cac phep so sanh vo nghia: a == a hoac x != x
pattern: $X == $X
3.2. Toán tử Ellipsis (...)
Đại diện cho "bất kỳ thứ gì" từ 0 đến nhiều phần tử:
- Trong danh sách đối số:
db.Query(..., $QUERY, ...)(bắt hàmdb.Querybất kể phía trước hoặc phía sau$QUERYcó bao nhiêu tham số khác). - Trong khối lệnh:
func (...) { ... $SINK(...) ... }(bắt sink xuất hiện ở bất kỳ dòng nào trong thân hàm).
4. Làm thế nào để viết Custom Rules từ Pattern cơ bản đến Taint Mode?
4.1. Rule 1: Bắt sử dụng StandardEvaluationContext trong Spring (Java)
Để ngăn chặn lỗ hổng SpEL Injection đã học ở Bài 02 — White-Box Audit Java & Spring Boot, ta viết rule cấm khởi tạo StandardEvaluationContext:
rules:
- id: spring-spel-dangerous-context
languages: [java]
severity: ERROR
message: "Phat hien su dung StandardEvaluationContext cho SpEL. Day la lo hong Critical dan toi RCE. Hay thay the bang SimpleEvaluationContext.forReadOnlyDataBinding()."
patterns:
- pattern: new StandardEvaluationContext(...)
- pattern-not-inside: |
class $TEST_CLASS { ... } # Bo qua cac file test unit
4.2. Rule 2: Taint Mode bắt SQL Formatting trong Golang
Chế độ mode: taint của Semgrep hiện thực hóa trọn vẹn mô hình Source $\to$ Sanitizer $\to$ Sink đã học ở Bài 01 — Nguyên lý Taint Analysis:
rules:
- id: go-sql-string-formatting-injection
mode: taint
languages: [go]
severity: ERROR
message: "Du lieu nguoi dung tu HTTP request bi ghep chuoi vao cau truy van SQL qua fmt.Sprintf(). Su dung Parameterized Query hoac Whitelist mapping."
pattern-sources:
# Source: Query params tu HTTP Request
- pattern: $R.URL.Query().Get(...)
- pattern: $R.FormValue(...)
pattern-sanitizers:
# Sanitizer: Cac ham chuyen doi so an toan
- pattern: strconv.Atoi(...)
- pattern: strconv.ParseInt(...)
pattern-sinks:
# Sink: fmt.Sprintf duoc truyen vao db.Query / db.Exec
- patterns:
- pattern: fmt.Sprintf($FMT, ...)
- pattern-inside: |
$DB.Query($CTX, $QUERY, ...)
# Semgrep rule definition attempt
rules:
- id: detect-exec-calls
languages: [python]
severity: WARNING
message: "Found dangerous exec call"
pattern: exec(user_input)5. Pitfalls & Giải pháp triển khai CI/CD
❌ Bẫy thường gặp: Viết rule quá lỏng lẻo gây "mỏi mệt cảnh báo" (Alert Fatigue)
Nếu viết rule chỉ đơn giản là pattern: fmt.Sprintf(...), Semgrep sẽ cảnh báo hàng nghìn lần trên cả các đoạn log hay format chuỗi thông thường. Các lập trình viên sẽ phớt lờ toàn bộ cảnh báo bảo mật.
✅ Giải pháp chuẩn mực: Kết hợp pattern-inside và Tích hợp CI/CD
- Thu hẹp ngữ cảnh với
pattern-inside: Chỉ cảnh báofmt.Sprintfkhi nó nằm bên trong một hàm truy vấn cơ sở dữ liệu hoặc nạp lệnh hệ thống. - Quy trình tích hợp GitHub Actions:
Thêm file workflow
.github/workflows/security-scan.ymlchạy tự động trên mỗi Pull Request:YAMLname: Security SAST Scan on: [pull_request] jobs: semgrep: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: semgrep/semgrep-action@v1 with: config: .semgrep/rules/
6. Liên hệ các bài khác
Kỹ năng viết Semgrep rule biến các kiến thức lý thuyết thành công cụ bảo vệ tự động:
- Thực hành viết rule độc lập: Áp dụng toàn bộ cú pháp Semgrep để vượt qua thử thách tại Bài 05 — Mini-challenge: Xây dựng Bộ Semgrep Rules chặn Lỗ hổng 0-Day.
- Hệ thống hóa nguyên lý Taint Flow: Ôn lại định nghĩa Source $\to$ Sink tại Bài 01 — Nguyên lý Taint Analysis: Từ Source đến Sink.
- Mẫu lỗi Java & Go: Xem lại các mẫu code nguy hiểm tại Bài 02 — White-Box Audit Java và Bài 03 — White-Box Audit Go.
7. Tóm tắt
- Semgrep là công cụ phân tích mã nguồn tĩnh hiện đại dựa trên cây cú pháp AST, viết quy tắc bằng chính cú pháp của ngôn ngữ lập trình.
- Metavariables (
$X) đại diện cho các biểu thức trừu tượng; Ellipsis (...) đại diện cho danh sách đối số hoặc dòng lệnh bất kỳ. - Chế độ
mode: taintliên kết chặt chẽ ba yếu tố:pattern-sources,pattern-sanitizersvàpattern-sinks. - Một rule bảo mật chất lượng cao phải có độ chính xác cao (Precision), không gây false positive làm gián đoạn quy trình phát triển.
- Tích hợp Semgrep vào CI/CD biến an toàn thông tin thành một cổng kiểm soát tự động trước khi mã nguồn được merge.
8. Tự kiểm tra
- Q1Vì sao Semgrep lại vượt trội hơn các công cụ quét mã nguồn tĩnh sử dụng biểu thức chính quy (Regex-based SAST)?
- Q2Toán tử Ellipsis (...) trong Semgrep có ý nghĩa gì khi đặt trong danh sách đối số của một hàm?
- Q3Trong Semgrep Taint Mode, vai trò của khối 'pattern-sanitizers' là gì?
- Q4Làm thế nào để loại trừ các file kiểm thử (test files) khỏi một rule Semgrep nhằm giảm false positive?
- Q5Điều gì xảy ra nếu cùng một Metavariable (ví dụ $VAR) xuất hiện hai lần trong một pattern Semgrep?
Bài tiếp theo: Mini-challenge: Xây dựng Bộ Semgrep Rules chặn Lỗ hổng 0-Day
White-Box Audit Go & Node.js: SQL Formatting và Prototype Pollution
Bài tiếpMini-challenge: Xây dựng Bộ Semgrep Rules chặn Lỗ hổng 0-Day
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Thảo luận & góp ý
Hỏi đáp về bài này