Protect source and CI
Minimize CI authority
Give workflows narrow token permissions, protect untrusted pull requests, pin external actions, and separate testing from publishing.
CI runs code and often holds credentials. A pull request may therefore be an attempt to execute code inside your trusted environment.
Every GitHub Actions job receives a GITHUB_TOKEN. Its default permissions are often broader than the job needs. Declare the minimum explicitly at the top of the workflow:
permissions:
contents: read
Now a compromised test dependency running in this job cannot push commits, open releases, or write packages with that token. A job that only runs tests needs read access and nothing else.
Treat fork pull requests as untrusted code
The standard pull_request trigger runs fork code without secrets and with a read-only token. That is the safe default.
The dangerous variant is pull_request_target. A workflow triggered with pull_request_target checks out code from an external contributor. That code now runs in the trusted base-repository context and may reach secrets or a write-capable token. If you must use it, never check out and execute the contributor’s code in that job. This exact pattern has burned many real projects.
Pin external actions
A workflow line like uses: someorg/setup-tool@v2 re-resolves on every run. Whoever controls that tag controls code in your CI. Pin third-party actions to a reviewed immutable commit SHA:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
The comment keeps the version readable; the SHA makes it immutable. Update pins deliberately, through reviewed pull requests — Dependabot can open them for you with package-ecosystem: "github-actions".
Separate testing from publishing
Tests and releases need different authority. Combining them makes every test execution a possible publishing event. Keep publish jobs in a separate workflow, gated behind protected events (a tag or release) and a protected environment that holds the release secrets. The test workflow should not even be able to see the npm token.
Map the trigger, token permissions, secrets, third-party actions, and environments for one workflow. Save a run showing that untrusted pull-request code receives no publishing credential. Then attempt a write operation from that job and capture the expected permission failure.
Lesson completed