Harden CI/CD workflow with least-privilege permissions - #2966
Conversation
The Build Release Branch workflow declared `permissions: write-all` and ran `npm ci` on every push to a non-main branch. Dependabot pushes trigger it (534 runs to date), so third-party install scripts executed on a runner holding a repo-write GITHUB_TOKEN — the default read-only boundary for Dependabot-triggered runs was explicitly overridden. Three layers, each independently sufficient for the Dependabot path: - Exclude `dependabot/**` from the push trigger. Dependency bumps do not need a build preview branch, and this removes untrusted code from the privileged workflow outright. - Replace `write-all` with per-job least privilege: `contents: read` to build, `contents: write` + `pull-requests: write` to publish and clean up. Checkout in the build job no longer persists credentials. - Split build from publish. The build job produces a tarball; the publish job downloads it and pushes without ever invoking npm, so the writable token and the build tooling never share a runner. Install is now `npm ci --ignore-scripts`. Only three packages in the tree declare an install script: `injected` (ours — its `copy-sjcl` postinstall now runs as an explicit step), `esbuild` (resolves its platform binary at runtime) and `fsevents` (macOS-only, optional; the runner is ubuntu). The node_modules cache key gains a `noscripts` segment so this tree is not restored by workflows that do run install scripts. Note this bounds token abuse, not artifact tampering: the build job can still influence its own output. Excluding Dependabot is what addresses the reported path. Co-Authored-By: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01Y5t5ARFHhq3V9kG7GHs7xM
[Beta] Generated file diffTime updated: Tue, 18 Aug 2026 15:23:34 GMT |
There was a problem hiding this comment.
Stale comment
Injected PR Evaluation: Web Compatibility & Security
Scope: This PR modifies only
.github/workflows/build-branch.yml(CI/CD supply-chain hardening). No files underinjected/,messaging/, orspecial-pages/runtime paths are changed.
Web Compatibility Assessment
No findings.
This workflow change does not alter injected script bundles, feature initialization, API overrides, DOM interaction, or platform entry points. There is zero user-facing web compatibility surface in this diff.
Security Assessment
No injected-runtime findings.
The diff does not touch
captured-globals.js, messaging transports, the message bridge,wrapper-utils.js,DDGProxy, feature code, or remote config. None of the injected-script threat models apply.CI supply-chain observations (informational, not injected-runtime risks):
File Lines Severity Note .github/workflows/build-branch.yml15–16, 23–24, 91–93, 298–299 info Least-privilege permissions: {}default with per-job opt-in (contents: readfor build,contents: write+pull-requests: writefor publish/clean_up) is a solid hardening pattern..github/workflows/build-branch.yml29–32 info persist-credentials: falseon the build job correctly prevents a write-capable token from persisting while third-party tooling runs..github/workflows/build-branch.yml51–63 info npm ci --ignore-scriptswith an explicitcopy-sjclstep blocks arbitrary dependency postinstall hooks. The threehasInstallScriptpackages (injected,esbuild,fsevents) are accounted for;esbuildresolves its platform binary at runtime on Linux..github/workflows/build-branch.yml86–107 info Splitting buildandpublishjobs socontents: writenever shares a runner withnpm ci/build tooling is the core security win of this PR. The publish job only unpacks a workflow artifact and runs git/gh-script — no dependency install..github/workflows/build-branch.yml9–12 info Ignoring dependabot/**prevents dependency-bump branches from triggering a job that eventually holds write credentials. Trade-off: Dependabot PRs will not receivepr-releases/build branches until merged/rebased onto a non-ignored branch..github/workflows/build-branch.yml49, 59–63 info Cache key bump ( noscripts) correctly invalidates stalenode_modulescaches from the pre---ignore-scriptsera. On cache hit,copy-sjclis skipped but the committedinjected/lib/sjcl.jssatisfies the build — same behavior as the prior workflow on cache hit.
Risk Level
Low Risk — CI workflow hardening only; no injected runtime code, API overrides, messaging, or security-sensitive browser-facing changes.
Recommendations
- No injected-runtime action required. Safe to merge from a C-S-S web-compat/security perspective.
- (info) Consider documenting in
CONTRIBUTING.mdthat Dependabot PRs intentionally skippr-releases/build branches due to the supply-chain separation, so reviewers know to test Dependabot bumps via the main CI workflow or local builds.- (info) If
injected/scripts/generateSJCL.jschanges without a lockfile bump, cache-hit builds will use the committedlib/sjcl.jsrather than regenerating — pre-existing behavior, but worth keeping in mind when reviewing SJCL-related changes.Sent by Cursor Automation: Web compat and sec
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9e980e8. Configure here.
|
This PR requires a manual review and approval from a member of one of the following teams:
|
Build Branch
Static preview entry points
QR codes (mobile preview)
Integration commandsnpm (Android / Extension): Swift Package Manager (Apple): .package(url: "https://github.com/duckduckgo/content-scope-scripts.git", branch: "pr-releases/claude/github-actions-supply-chain-cbejig")git submodule (Windows): git -C submodules/content-scope-scripts fetch origin pr-releases/claude/github-actions-supply-chain-cbejig
git -C submodules/content-scope-scripts checkout origin/pr-releases/claude/github-actions-supply-chain-cbejigPin to exact commitnpm (Android / Extension): Swift Package Manager (Apple): .package(url: "https://github.com/duckduckgo/content-scope-scripts.git", revision: "fb6bc68a11202356fa4d15063894c8e66d4fa526")git submodule (Windows): git -C submodules/content-scope-scripts fetch origin pr-releases/claude/github-actions-supply-chain-cbejig
git -C submodules/content-scope-scripts checkout fb6bc68a11202356fa4d15063894c8e66d4fa526 |
The workflow change made the CONTRIBUTING.md exclusion list inaccurate. Add `dependabot/*` and explain why those branches are skipped. Also corrects two references to `build-pr.yml`, which does not exist — the workflow is `build-branch.yml`. Co-Authored-By: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01Y5t5ARFHhq3V9kG7GHs7xM
There was a problem hiding this comment.
Injected PR Evaluation: Web Compatibility & Security
Assessed at: 7900d591 (synchronize) — delta since prior review is CONTRIBUTING.md documentation only.
Web Compatibility Assessment
No findings.
This PR touches only .github/workflows/build-branch.yml and CONTRIBUTING.md. There are no changes under injected/, messaging/, special-pages/, or platform entry points. None of the web-compatibility evaluation categories (API surface fidelity, prototype chain integrity, DOM interaction, timing, platform-specific breakage, third-party script compatibility) apply.
Security Assessment
No injected-runtime security findings.
The changed files are CI/CD infrastructure and contributor documentation — outside the page-injection threat model. The workflow changes are supply-chain hardening (positive security posture), not regressions:
| File | Lines | Severity | Notes |
|---|---|---|---|
.github/workflows/build-branch.yml |
15–16, 23–24, 29–32 | info | Workflow-level permissions: {} with per-job least-privilege (contents: read for build, contents: write + pull-requests: write for publish/clean_up). |
.github/workflows/build-branch.yml |
86–107 | info | Build/publish job split via artifact handoff — publish runner never executes third-party npm ci tooling, isolating the contents: write token. |
.github/workflows/build-branch.yml |
29–32, 57 | info | persist-credentials: false on build checkout + npm ci --ignore-scripts blocks lifecycle-hook execution during dependency install. |
.github/workflows/build-branch.yml |
59–63 | info | Explicit npm run copy-sjcl -w injected re-runs the one required postinstall script after --ignore-scripts. |
.github/workflows/build-branch.yml |
9–12 | info | dependabot/** excluded from push trigger — prevents auto-publishing build branches from dependency-bump PRs that introduce third-party code. |
CONTRIBUTING.md |
116, 139, 149 | info | Documents the Dependabot exclusion and corrects workflow filename reference (build-branch.yml). No runtime impact. |
Info — cache key bump (noscripts): Line 49 invalidates cached node_modules from pre---ignore-scripts installs. Correct hygiene; no security concern.
Info — SJCL on cache hit: Generate SJCL bundle (lines 59–63) is gated on cache miss. A warm cache from a prior successful run already contains the generated bundle; no gap identified.
Risk Level
Low Risk — CI/CD workflow hardening and contributor documentation only; zero injected-runtime, messaging, or special-pages code changes.
Recommendations
No blocking items. Optional follow-ups (CI hygiene, not injected-security):
- Verify end-to-end — Confirm a non-Dependabot feature-branch push still produces a
pr-releases/<branch>artifact and PR comment after the build/publish split. - Monitor first cache miss — After merge, watch one cold-cache run to confirm
copy-sjcl+npm run buildsucceed under--ignore-scripts.
Sent by Cursor Automation: Web compat and sec



Description
Refactors the GitHub Actions workflow to follow the principle of least privilege and improve security isolation:
Permission scoping: Changes from
permissions: write-alltopermissions: {}at the workflow level, with each job explicitly declaring only the permissions it needs (contents: readfor build,contents: write+pull-requests: writefor publish).Job separation: Splits the
buildandpublishjobs so that the token with write access to the repository never runs on the same runner as third-party build tooling. Build output is passed via artifact upload/download.Build hardening:
persist-credentials: falseto checkout to prevent build scripts from accessing push credentialsnpm ci --ignore-scriptsto block lifecycle hooks from untrusted packagesinjectedworkspace's postinstall script (copy-sjcl) after install--ignore-scriptsbehaviorArtifact handling: Packages build output as a tarball to ensure consistent layout and efficient transfer between jobs.
Branch filtering: Adds
dependabot/**branches to the trigger list with a comment explaining the security rationale (dependency bumps are where third-party code enters the repo).Cleanup job: Adds explicit
permissions: contents: writeto the cleanup job.These changes reduce the attack surface by ensuring that if build tooling is compromised, it cannot push code or modify the repository.
Testing Steps
Checklist
https://claude.ai/code/session_01Y5t5ARFHhq3V9kG7GHs7xM
Note
Medium Risk
Changes CI security boundaries and artifact handoff for
pr-releasespublishing; behavior should match prior flows but misconfiguration could break PR build branches or leave publish credentials on build runners.Overview
Hardens
build-branch.ymlby replacing workflow-widewrite-allwith per-job permissions, splitting build and publish onto separate runners, and tightening how dependencies and artifacts are handled.The build job now checks out with
persist-credentials: false, installs vianpm ci --ignore-scripts(with an explicitcopy-sjclstep forinjected), bumps the cache key for that install mode, and uploads a singlebuild-output.tar.gzartifact (short retention) instead of pushing from the same job.A new publish job downloads that tarball, unpacks it, and runs the existing push-to-
pr-releases/<branch>and PR annotation logic with onlycontents: writeandpull-requests: write. Dependabot branches are added tobranches-ignoreso third-party dependency bumps do not run alongside a write-capable publish path. clean_up explicitly requestscontents: write.CONTRIBUTING.md renames the workflow to
build-branch.yml, documents the dependabot exclusion, and notes testing Dependabot bumps via normal CI or local builds instead.Reviewed by Cursor Bugbot for commit 7900d59. Bugbot is set up for automated code reviews on this repo. Configure here.