Ngừng phát hành lỗ hổng: Tự động hóa SAST với Semgrep

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Cuộc gọi đánh thức lúc 2 giờ sáng

Tôi đã từng trải qua một buổi sáng sớm điên cuồng để chống lại một cuộc tấn công brute-force vào máy chủ production. Trải nghiệm đó đã thay đổi quan điểm của tôi: bảo mật kiểu phản ứng (reactive security) là một cuộc chơi nắm chắc phần thua. Chờ đợi một cảnh báo được kích hoạt có nghĩa là bạn đã chậm chân rồi.

Bạn phải nhúng các cơ chế phòng thủ vào chính mã nguồn. Dữ liệu ngành từ NIST cho thấy việc sửa một lỗ hổng trên môi trường production có thể tốn kém gấp 30 đến 100 lần so với việc phát hiện nó trong quá trình phát triển. Đối với một đội ngũ nhỏ, đó là sự khác biệt giữa một bản vá nhẹ nhàng và một tuần mất năng suất lao động.

Tôi đã chứng kiến nhiều đội ngũ vô tình đẩy các mã bí mật (secret keys) của AWS hoặc các lỗ hổng SQL injection lên vì họ đang chạy đua với deadline. Review code thủ công là rất quan trọng, nhưng con người ai cũng có lúc sai sót, đặc biệt là vào lúc 4 giờ chiều thứ Sáu. Đây là lúc Kiểm thử Bảo mật Ứng dụng Tĩnh (SAST) lấp đầy khoảng trống đó. Tôi nhận thấy Semgrep là công cụ hiệu quả nhất để ngăn chặn những rò rỉ này trước khi chúng tiếp cận nhánh chính (main branch) của bạn.

Tại sao các lỗ hổng vẫn bị bỏ sót

Nợ bảo mật thường không phải là kết quả của sự cẩu thả. Nó xảy ra vì môi trường phát triển thay đổi nhanh hơn các bước kiểm tra. Tôi đã xác định được ba nút thắt cổ chai cụ thể gây ra hầu hết các vụ rò rỉ:

  • Đánh giá chủ quan: Một lập trình viên có thể phát hiện việc thiếu kiểm tra dữ liệu đầu vào (input validation), trong khi người khác chỉ kiểm tra quy tắc đặt tên. Nếu không có tự động hóa, vị thế bảo mật của bạn hoàn toàn phụ thuộc vào người đang review PR.
  • Điểm mù của Framework: Các stack hiện đại rất phức tạp. Thật dễ dàng để quên rằng một phương thức cụ thể, như dangerouslySetInnerHTML trong React hoặc mark_safe trong Django, sẽ mở ra con đường trực tiếp cho tấn công XSS.
  • Nút thắt cổ chai “Trước khi phát hành”: Việc chỉ chạy các bản quét nặng trên môi trường staging sẽ tạo ra một đống lỗi khổng lồ ngay khi bạn muốn phát hành. Nó biến bảo mật thành một rào cản thay vì là một tính năng.

Vấn đề về vòng lặp phản hồi

Lập trình viên không cần thêm những bản báo cáo PDF dài 500 trang. Các công cụ bảo mật cũ thường hoạt động như những “hộp đen” mất tới ba giờ để chạy và trả về 40% kết quả dương tính giả (false positives). Khi một công cụ báo động giả quá nhiều, các kỹ sư sẽ không còn bận tâm đến nó nữa. Để đạt hiệu quả, phản hồi bảo mật phải nhanh chóng, minh bạch và nằm ngay tại nơi code hiện diện—bên trong GitHub, GitLab hoặc Bitbucket.

So sánh Semgrep với các công cụ truyền thống

Tôi đã thử nghiệm nhiều trình quét trước khi chọn Semgrep. Hầu hết các công cụ truyền thống coi mã nguồn như văn bản thuần túy, nhưng Semgrep hiểu cấu trúc của mã bằng cách phân tích Cây Cú pháp Trừu tượng (AST). Điều này cho phép nó nhận ra rằng exec(user_input) là một rủi ro ngay cả khi các biến được định nghĩa cách xa nhau hàng chục dòng.

Tính năng Review thủ công SAST truyền thống (vd: SonarQube) Semgrep (SAST hiện đại)
Tốc độ quét Vài ngày Trên 20 phút < 2 phút
Ngôn ngữ quy tắc Trí tuệ con người Độc quyền/Java YAML đơn giản
Mức độ nhiễu Trung bình Cao (Dương tính giả) Thấp (Độ chính xác cao)
Thời gian thiết lập Tức thì Vài giờ/Vài ngày 5 phút

Semgrep có thể xử lý hơn 1.000 quy tắc bảo mật đối với 100.000 dòng code chỉ trong vài giây. Tốc độ đó khiến nó trở nên khả thi cho mọi commit, chứ không chỉ cho các bản build hàng đêm.

Tích hợp Semgrep vào GitHub Actions

Mục tiêu là biến Semgrep thành một “người gác cổng”. Nếu một PR gây ra một lỗ hổng nghiêm trọng, bản build sẽ thất bại ngay lập tức. Đây là workflow tôi sử dụng cho các môi trường production của mình.

1. Kiểm thử cục bộ

Phát hiện lỗi ngay trên máy của bạn sẽ nhanh hơn việc chờ đợi CI runner. Hãy cài đặt Semgrep qua Homebrew hoặc Pip để bắt đầu:

# Cài đặt qua pip
python3 -m pip install semgrep

# Chạy quét bằng các quy tắc đã được cộng đồng kiểm chứng
semgrep scan --config auto

Flag --config auto là một “vị cứu tinh”. Nó tự động phát hiện các ngôn ngữ trong dự án của bạn và tải về các chính sách bảo mật liên quan từ Semgrep Registry.

2. Workflow cho GitHub Actions

Hãy đưa cấu hình này vào file .github/workflows/semgrep.yml. Nó sẽ được kích hoạt trên mỗi pull request để đảm bảo không có lỗ hổng mới nào lọt vào nhánh main của bạn.

name: Quét Bảo mật

on:
  pull_request:
    branches: ["main"]
  push:
    branches: ["main"]

jobs:
  semgrep_scan:
    runs-on: ubuntu-latest
    container:
      image: returntocorp/semgrep
    steps:
      - name: Checkout mã nguồn
        uses: actions/checkout@v4

      - name: Chạy Semgrep
        run: semgrep scan --config p/default --error

      - name: Tạo báo cáo bảo mật
        run: semgrep scan --config p/default --sarif --output=semgrep.sarif

      - name: Tải lên tab Bảo mật của GitHub
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: semgrep.sarif
        if: always()

Flag --error là phần quan trọng nhất của script này. Nó bắt buộc trả về mã thoát khác không khi tìm thấy các vấn đề có độ nghiêm trọng cao, từ đó chặn việc merge cho đến khi mã nguồn được sửa lỗi.

Quy tắc tùy chỉnh cho tiêu chuẩn của đội ngũ

Mỗi đội ngũ đều có những quy tắc “không được làm” riêng. Có thể bạn muốn cấm băm MD5 vì nó dễ bị tấn công xung đột, hoặc có lẽ bạn muốn đảm bảo không ai sử dụng sai một thư viện nội bộ cụ thể. Bạn có thể viết một quy tắc tùy chỉnh chỉ trong vài phút.

Tạo file semgrep-rules.yaml:

rules:
  - id: ban-insecure-hashing
    patterns:
      - pattern: hashlib.md5(...)
    message: "MD5 không an toàn. Hãy sử dụng SHA-256 cho tất cả các yêu cầu băm."
    languages: [python]
    severity: ERROR

Điều này biến chính sách bảo mật nội bộ của bạn thành mã thực thi mà không bao giờ quên bất kỳ quy tắc nào.

Quản lý các trường hợp dương tính giả

Các công cụ bảo mật không phải là nhà ngoại cảm. Đôi khi một lệnh gọi exec() bị gắn cờ lại thực sự cần thiết cho một tác vụ quản trị cụ thể. Thay vì tắt trình quét, hãy sử dụng các lệnh bỏ qua cục bộ để duy trì dấu vết kiểm tra (audit trail).

# nosemgrep: python.lang.security.audit.dangerous-exec
exec(internal_config_string) # Đã được xác thực trong quá trình khởi động

Tôi luôn yêu cầu đội ngũ của mình thêm một comment giải thích cho thẻ # nosemgrep. Điều này giúp việc kiểm tra bảo mật trong tương lai nhanh hơn nhiều.

Xây dựng văn hóa bảo mật

Semgrep không chỉ là một công cụ săn lỗi; nó còn là một công cụ giảng dạy. Khi một lập trình viên thấy cảnh báo bảo mật trong PR của họ, họ sẽ học về lỗ hổng đó ngay lập tức. Điều này tạo ra một vòng lặp phản hồi liên tục giúp cải thiện kỹ năng lập trình của đội ngũ theo thời gian. Kể từ khi triển khai “người gác cổng” tự động 24/7 này, tôi đã thấy sự sụt giảm đáng kể các lỗ hổng nghiêm trọng lọt vào môi trường staging, và cuối cùng tôi đã có thể ngủ ngon giấc cả đêm.

Share: