GitHub Actions and Untrusted Pull Requests: Keep Privilege Out of Tests

Review GitHub Actions event triggers, checkout sources, artifacts, and runner permissions so untrusted pull-request code cannot inherit deployment privileges.

In this article

Running a contributor's tests means running code you haven't fully reviewed. That may be acceptable in a restricted test environment, but it becomes dangerous when the same job has deployment credentials, repository write access, or a persistent runner connected to internal systems.

The GitHub Actions security question isn't simply whether the workflow file looks familiar. Ask which event starts it, which revision supplies the code, and which privileges are available when that code executes. Those three facts define the trust boundary.

Inventory the triggers and execution sources

List workflows responding to pull requests, target-context events, completed workflows, and manual dispatches. For each job, record the workflow source, checkout reference, scripts executed, token permissions, secrets, and runner type.

GitHub's secure-use reference explains risks around untrusted inputs and privileged workflow contexts. A job triggered by a familiar event can still execute attacker-controlled data or code through a later step.

Include package installation and build scripts in the inventory. “We only run tests” may still execute lifecycle scripts or imported helpers from the proposed change. The boundary includes everything the test command causes to run, not only the command text itself.

Separate untrusted testing from privileged work

Use a restricted job for untrusted contributions, with the minimum permissions and no deployment credentials. Keep release jobs dependent on reviewed, trusted revisions and the project's approved release process. Don't hand the test environment a secret merely because one test would be easier with it.

GitHub's pullrequesttarget safety guidance addresses the danger of checking out and executing untrusted pull-request code in a more privileged context. Current platform safeguards help, but they don't make every custom script safe.

Review manual checkout commands and third-party actions too. A workflow can bypass a built-in protection by fetching a revision through another route. The right response is to preserve the trust boundary, not to disable a warning until the job runs.

Treat pull-request metadata as input

Titles, descriptions, branch names, and other contributor-controlled fields can influence scripts. Don't interpolate them directly into shell source. Pass them through the supported data channel and quote them appropriately when a command needs them.

For a synthetic example, a label-processing job should decide among fixed supported labels rather than construct commands from arbitrary text. A label is data describing the request; it isn't permission to choose a shell operation.

Use Text Diff to review sanitised workflow changes. Focus on the places where expressions enter commands and where privileges become available. A tiny changed reference can matter more than a large formatting update elsewhere.

Review artifacts and caches as trust transfers

A privileged follow-up job may download an artifact produced by an untrusted job. That artifact is still untrusted. Don't execute scripts from it or assume its claimed filename and provenance make it safe.

Define what the privileged job accepts: a bounded report format, a verified build artifact from a trusted revision, or another explicit contract. Validate the content and source before using it. Keep data-only reporting separate from executable release material.

Caches can also carry content across runs. Review whether an untrusted context can populate something a privileged context later executes or imports. Don't assume that a cache key designed for convenience also enforces a security boundary.

Protect runner state

A persistent self-hosted runner may retain files, credentials, or access to internal services across jobs. An untrusted job running there can affect more than its visible checkout directory. Use an appropriate isolated runner design and review its network reachability.

Clean-up commands alone aren't proof of isolation. A compromised process may alter state you didn't anticipate. Test the actual lifecycle of the runner, including how it is provisioned, destroyed or reset, and prevented from sharing sensitive resources.

Record which jobs are allowed on each runner class. A label such as test isn't a security policy unless scheduling and access controls enforce it. Keep production-capable runners away from arbitrary contributor code.

Test the denied path

Use a controlled test repository and synthetic contributions to verify that untrusted jobs cannot read deployment secrets, obtain privileged tokens, or reach internal services. Avoid using real secret values as test markers; use harmless sentinels and approved diagnostic checks.

Also test a follow-up workflow that receives a report from the untrusted job. Confirm it treats the report as data and doesn't run arbitrary contents. Document which source revision each stage trusts.

For sanitised permission manifests, JSON Diff can highlight changes. It doesn't audit workflow execution or prove that a runner was isolated. Keep negative tests and platform configuration evidence alongside the review.

Keep deployment identity separate

Federated credentials can reduce static-secret exposure, but a privileged job can still misuse short-lived access if it runs untrusted code. See GitHub Actions OIDC trust policies for reviewing the cloud identity boundary.

Set a clear owner for workflow security changes and include CI configuration in normal code review. A repository's protection rules should cover release logic as well as application code. Recheck assumptions when platform defaults or actions change.

Conclusion

Untrusted pull requests deserve useful tests, not inherited deployment power. Review triggers, checkout sources, metadata, artifacts, caches, and runners as connected trust decisions. Keep privileged execution tied to reviewed code and prove that denied contexts remain denied.

Advertisement
GitHub Actions: Untrusted Pull Request Safety | Duck Cloud