Mấy hôm nay mình đang dọn lại một số workflow GitHub Actions thì thấy GitHub công bố thêm lớp bảo vệ mới: một số workflow đáng ngờ trong repository công khai sẽ bị giữ lại, chờ người có quyền ghi duyệt trên web rồi mới chạy. Nghe khá yên tâm, nhưng câu hỏi quan trọng hơn là: lúc nút Approve and run hiện ra, mình cần nhìn vào đâu trước khi bấm?
Mình đọc lại tài liệu chính thức, sau đó dựng hai workflow nhỏ để kiểm tra tĩnh trên Windows. Kết quả hơi thú vị: cả file an toàn lẫn file có tổ hợp rủi ro đều được actionlint cho qua. Nói cách khác, YAML xanh chưa có nghĩa là workflow an toàn. Bài này ghi lại đúng những gì mình đã thử và checklist mình sẽ dùng cho các pull request từ fork.
Mục lục [Hiển thị]
GitHub đang giữ workflow nào để chờ duyệt?
Theo thông báo ngày 28/07/2026 của GitHub, GitHub Actions bắt đầu tự động giữ một số workflow có dấu hiệu chưa đáng tin cậy trong repository công khai trên github.com. Workflow chỉ tiếp tục sau khi một collaborator có quyền ghi duyệt bằng phiên đăng nhập trên giao diện web.
Điểm mình thích là lớp bảo vệ này hoạt động tự động, maintainer không phải bật thêm cấu hình. Tuy nhiên, “bị giữ” không đồng nghĩa với “đã xác định là mã độc”, và một workflow không bị giữ cũng không có nghĩa là nó an toàn tuyệt đối. GitHub đang thêm một điểm dừng để con người kiểm tra, không thay con người đưa ra quyết định.
| GitHub hỗ trợ | Maintainer vẫn phải làm |
|---|---|
| Giữ một số workflow đáng ngờ trước khi chạy | Đọc thay đổi trong PR và file workflow |
| Yêu cầu người có quyền ghi xác nhận trên web | Kiểm tra quyền token, secret và code được checkout |
| Tạo thêm một điểm kiểm soát tự động | Quyết định duyệt, yêu cầu sửa hoặc đóng PR |
Phạm vi trong thông báo là repository công khai trên github.com; GitHub Enterprise Server không nằm trong đợt áp dụng này. Nếu đội của bạn cần kiểm soát rộng hơn theo actor hoặc event, GitHub còn có workflow execution protections trong phần Policies. Hai cơ chế liên quan nhau nhưng không nên hiểu là một.

Mình đã dựng bài test như thế nào?
Mình tạo một thư mục cục bộ với hai file trong .github/workflows. Không file nào được push lên GitHub, không dùng token thật, không có secret và cũng không chạy trên runner. Mục tiêu chỉ là xem một công cụ lint phổ biến có giúp phân biệt lỗi cú pháp với lỗi thiết kế bảo mật hay không.
File đầu tiên dùng event pull_request, đặt quyền đọc tối thiểu và không giữ credential sau bước checkout:
name: Safe pull request check
on:
pull_request:
permissions:
contents: read
jobs:
inspect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- run: echo "Static check only"
File thứ hai cố tình ghép ba tín hiệu cần review: event pull_request_target, quyền contents: write và checkout đúng commit từ fork. Phần lệnh bên trong vẫn chỉ in một chuỗi cố định, vì mình không cần chạy mã không tin cậy để chứng minh vấn đề.
name: Workflow that needs manual review
on:
pull_request_target:
permissions:
contents: write
jobs:
inspect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
- run: echo "This file is linted locally and is never executed"
Mình tải binary Windows AMD64 của actionlint từ release chính thức, đối chiếu SHA-256 với file checksum rồi mới giải nén. Phiên bản tại thời điểm thử là 1.7.12, build bằng Go 1.26.1. Lệnh kiểm tra như sau:
actionlint.exe .github/workflows/safe-pr.yml `
.github/workflows/needs-review.yml
Write-Output $LASTEXITCODE
Kết quả là 0 và không có cảnh báo cho cả hai file. Đây không phải lỗi của actionlint: công cụ này rất hữu ích để bắt lỗi syntax, expression, input của action và shell script. Nhưng nó không thể thay maintainer đánh giá toàn bộ trust boundary của một pipeline. Tình huống này khá giống điều mình từng thấy khi test xanh nhưng chất lượng code vẫn chưa chắc ổn.

Vì sao pull_request_target cần soi kỹ?
pull_request_target chạy trong ngữ cảnh của nhánh gốc, không phải nhánh trong fork. Nó hữu ích cho các tác vụ cần quyền cao như gắn nhãn, bình luận hoặc quản lý PR. Vấn đề xuất hiện khi workflow có quyền cao lại checkout và thực thi code do người gửi PR kiểm soát.
Tài liệu Secure use của GitHub khuyến nghị tránh dùng pull_request_target nếu không thực sự cần, đồng thời không checkout code không tin cậy trong workflow đặc quyền. GitHub cũng đã thêm mặc định an toàn hơn cho actions/checkout@v7, nhưng lớp bảo vệ ở một action không thể sửa mọi cách tổ chức pipeline sai.
Mình thường tách nhu cầu thành hai câu hỏi. Nếu job chỉ cần build và test code trong PR, dùng pull_request với quyền đọc thường đã đủ. Nếu job cần quyền để gắn nhãn hoặc bình luận, nó không nên build code vừa checkout từ fork. Chỗ này mà trộn hai việc vào một job thì rất dễ tới công chuyện.
Checklist trước khi bấm Approve and run
1. Đọc event trước, đừng đọc lệnh trước
Nhìn vào khối on để biết workflow chạy trong ngữ cảnh nào. Với pull_request_target, workflow_run hoặc workflow_dispatch, mình kiểm tra kỹ hơn vì các event này có thể đi cùng quyền hoặc dữ liệu mà workflow kiểm tra PR thông thường không có.
Sau đó mình xem PR có sửa chính file workflow hay không. Nếu người gửi đồng thời thay workflow, script build, dependency và file cấu hình, phạm vi review phải bao phủ tất cả các đường mà job có thể gọi tới, không chỉ vài dòng YAML hiện trên màn hình.
2. Xác định chính xác code nào được checkout
Tìm mọi bước dùng actions/checkout, đặc biệt là các giá trị repository và ref. Trong workflow đặc quyền, checkout github.event.pull_request.head.sha nghĩa là đưa code do fork kiểm soát vào workspace. Nếu bước tiếp theo chạy npm install, Composer script, Makefile hoặc test command thì code đó có thể được thực thi gián tiếp.
Không thấy lệnh đáng ngờ ngay trong YAML chưa đủ. Script có thể nằm trong package.json, composer.json, action cục bộ hoặc file shell khác. Khi viết bài giới thiệu repository, mình vẫn xem cấu trúc dự án và giấy phép như trong lần thử Superfile trên terminal; với workflow có quyền ghi thì bước này còn quan trọng hơn.
3. Thu hẹp GITHUB_TOKEN
Tìm permissions ở cả cấp workflow lẫn từng job. Nếu không có nhu cầu ghi, mình đặt mặc định:
permissions:
contents: read
Job nào cần quyền riêng thì cấp đúng quyền ở job đó. contents: write, pull-requests: write, packages: write hoặc id-token: write đều cần lý do cụ thể. Nguyên tắc ở đây khá đơn giản: code chưa tin cậy không nên cầm một chiếc chìa khóa rộng hơn công việc nó phải làm.
4. Kiểm tra secret và dữ liệu đầu vào
Mình tìm secrets., environment được bảo vệ và mọi expression lấy dữ liệu từ github.event. Dữ liệu như title, branch name hoặc body của PR là đầu vào do người dùng kiểm soát. Nếu nhét expression trực tiếp vào shell script, nó có thể tạo ra script injection; tài liệu GitHub khuyên đưa dữ liệu qua biến môi trường trung gian rồi xử lý như dữ liệu.
Với job nhận code từ fork, lựa chọn an toàn nhất thường là không truyền secret. Trường hợp bắt buộc phải truy cập dịch vụ ngoài nên được tách sang workflow đặc quyền, chỉ nhận artifact hoặc dữ liệu đã kiểm tra và giới hạn quyền của credential phía dịch vụ.
5. Đừng quên action bên thứ ba và cache
Action bên thứ ba cũng là code chạy trong pipeline. Mình ưu tiên action từ nguồn rõ ràng và pin theo commit SHA khi mức rủi ro yêu cầu. Major tag như @v4 tiện nâng cấp, nhưng nó là một tham chiếu có thể di chuyển; đây là đánh đổi cần được chọn có chủ đích.
GitHub đã chuyển cache sang chế độ chỉ đọc cho một số trigger không tin cậy theo thông báo ngày 26/06/2026. Đây là lớp bảo vệ tốt, nhưng cache read-only không xử lý quyền token quá rộng, secret bị lộ hoặc code từ fork được chạy trong ngữ cảnh đặc quyền.
Một cấu hình tối thiểu mình sẽ dùng
Với job chỉ build và test pull request, cấu hình dưới đây là điểm bắt đầu dễ hiểu. Nó dùng event pull_request, token chỉ đọc và không lưu credential Git vào cấu hình cục bộ sau checkout:
name: Pull request checks
on:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- run: npm ci
- run: npm test
Đoạn này không phải công thức an toàn tuyệt đối. npm ci vẫn chạy lifecycle script từ dependency và project, runner vẫn thực thi code trong PR, còn action dùng tag thay vì SHA. Nó chỉ minh họa cách giữ workflow kiểm tra PR ở ngữ cảnh ít quyền. Với self-hosted runner, mình sẽ thận trọng hơn nữa vì code không tin cậy chạy ngay trên hạ tầng của mình.
Khi nào cần workflow execution protection?
Repository cá nhân ít contributor có thể bắt đầu bằng approval policy, quyền token tối thiểu và review file workflow. Khi tổ chức có nhiều nhóm, nhiều người được cấp quyền ghi hoặc nhiều event thủ công, workflow execution protections giúp đưa luật ra khỏi từng file YAML.
GitHub cho phép tạo ruleset để kiểm soát actor và event nào được quyền kích hoạt workflow. Mình thấy hướng này hợp khi cần cấm pull_request_target trên diện rộng, giới hạn workflow_dispatch cho maintainer hoặc chạy chế độ evaluate trước khi cưỡng chế. Tuy vậy, cần kiểm tra gói GitHub và phạm vi áp dụng thực tế của tổ chức trước khi thiết kế quy trình.
Nếu đội đang xử lý một đợt cập nhật runtime, bài Node.js hoãn bản vá bảo mật tháng 7/2026 có checklist cho dependency và lịch release. Còn lớp bảo vệ trong bài này nằm sớm hơn: kiểm soát code nào được bước vào pipeline và nó được cầm quyền gì.
Checklist 60 giây mình sẽ giữ lại
- PR có sửa workflow, script build hoặc dependency không?
- Workflow chạy bằng event nào, có phải
pull_request_targetkhông? actions/checkoutđang lấy code từ base hay từ fork?GITHUB_TOKENcó quyền ghi nào không cần thiết không?- Job có nhận secret, environment hoặc OIDC token không?
- Dữ liệu từ PR có được đưa thẳng vào shell script không?
- Action bên thứ ba và cache có nằm trong trust boundary mình chấp nhận không?
Sau lần thử này, điều mình nhớ nhất là exit code 0 chỉ nói workflow hợp lệ theo những gì linter kiểm tra. Nó không trả lời câu hỏi “mình có nên tin code này với quyền này hay không?”. GitHub giữ workflow đáng ngờ là một điểm dừng rất đáng giá, còn quyết định bấm chạy vẫn phải dựa trên review.
Nguyên tắc mình sẽ dùng khá ngắn: nếu chưa chỉ ra được code nào sắp chạy, chạy ở đâu và cầm quyền gì, mình chưa duyệt. Anh em đang quản lý repository công khai có thể thử checklist trên với một workflow cũ; nhiều khi chỉ cần đọc lại khối on và permissions đã thấy ngay chỗ cần dọn.