From e2e2f65a0327c16d94c3ba788e241c84d1ce03e7 Mon Sep 17 00:00:00 2001 From: Aaron Sachs Date: Fri, 4 Sep 2026 00:28:12 +0000 Subject: [PATCH 1/4] fix(release): restore persist-credentials:false, re-auth only for release ops CodeRabbit catch (CWE-250, found on node-connectwise-automate rollout, live-verified there): the checkout step's default persisted credential stayed live through npm ci/build/test. Restores persist-credentials: false (the prior design had it) and re-authenticates in one place, right after the untrusted npm lifecycle completes, for the git commands (fetch --tags, the two pushes) that need it. task_1788457898992. --- .github/workflows/release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c45397a..1a53cd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,6 +64,12 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + # CodeRabbit catch (CWE-250, task_1788457898992): the default + # persisted credential would stay live through npm ci/build/test + # below, so a compromised dependency's lifecycle script could + # misuse it to push. Re-authenticate explicitly, after those + # steps run, only for the git commands that actually need it. + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -81,6 +87,15 @@ jobs: - name: Run tests run: npm test + # Re-authenticate AFTER the untrusted npm lifecycle (ci/build/test) + # has already run, so nothing during those steps can reach this + # credential. Every git command below this point (fetch --tags, the + # two `git push` calls) needs it -- restoring it once, here, keeps + # each of those call sites unchanged rather than repeating auth setup + # per push. + - name: Re-authenticate git for release operations + run: git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" + # Each of the three publish artifacts (tag, npm package, GitHub release) # is checked independently rather than inferring all-or-nothing status # from the tag alone (CodeRabbit catch, review of this PR: a transient From bc9e11096482d23b6615e01e37219bae185501cd Mon Sep 17 00:00:00 2001 From: Aaron Sachs Date: Fri, 4 Sep 2026 00:39:19 +0000 Subject: [PATCH 2/4] fix(release): use inline http.extraheader instead of git remote set-url CodeRabbit catch (CWE-522): set-url writes the token into .git/config, readable by any later process in the job. Authenticate each git network call individually via -c http.extraheader instead -- nothing persists to disk. task_1788457898992. --- .github/workflows/release.yml | 43 ++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a53cd5..aa69938 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,8 +67,10 @@ jobs: # CodeRabbit catch (CWE-250, task_1788457898992): the default # persisted credential would stay live through npm ci/build/test # below, so a compromised dependency's lifecycle script could - # misuse it to push. Re-authenticate explicitly, after those - # steps run, only for the git commands that actually need it. + # misuse it to push. Each git network call downstream instead + # authenticates individually via an inline `-c http.extraheader` + # (never written to .git/config — see the second CodeRabbit catch, + # CWE-522, at "Determine mode" below). persist-credentials: false - name: Setup Node.js @@ -87,15 +89,6 @@ jobs: - name: Run tests run: npm test - # Re-authenticate AFTER the untrusted npm lifecycle (ci/build/test) - # has already run, so nothing during those steps can reach this - # credential. Every git command below this point (fetch --tags, the - # two `git push` calls) needs it -- restoring it once, here, keeps - # each of those call sites unchanged rather than repeating auth setup - # per push. - - name: Re-authenticate git for release operations - run: git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" - # Each of the three publish artifacts (tag, npm package, GitHub release) # is checked independently rather than inferring all-or-nothing status # from the tag alone (CodeRabbit catch, review of this PR: a transient @@ -104,6 +97,17 @@ jobs: # steady state, and never retries the artifacts that actually failed). # This makes a rerun after a partial failure resume exactly the # missing steps instead of silently skipping them. + # + # Git auth note (CodeRabbit, CWE-250 then CWE-522, task_1788457898992): + # persist-credentials is false on checkout above, and this step's own + # `git fetch --tags` is the first git network call after the untrusted + # npm lifecycle. A first pass re-authenticated via `git remote + # set-url`, but that WRITES the token into .git/config where any later + # process in the job could read it back off disk. Using a `-c + # http.extraheader` on the git invocation itself instead scopes the + # credential to that one command's process environment -- nothing + # persists to a file. Every git network call in this workflow uses + # this same inline pattern; none set the remote URL. - name: Determine mode (publish vs prepare) id: mode env: @@ -111,7 +115,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail - git fetch --tags + AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" + git -c http.extraheader="$AUTH_HEADER" fetch --tags VERSION=$(jq -r '.version' package.json) PKG_NAME=$(jq -r '.name' package.json) @@ -140,11 +145,14 @@ jobs: - name: "Publish: tag" if: steps.mode.outputs.mode == 'publish' && steps.mode.outputs.tag_exists == 'false' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail VERSION="${{ steps.mode.outputs.version }}" + AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" git tag "v${VERSION}" - git push origin "v${VERSION}" + git -c http.extraheader="$AUTH_HEADER" push origin "v${VERSION}" - name: "Publish: npm publish" if: steps.mode.outputs.mode == 'publish' && steps.mode.outputs.npm_published == 'false' @@ -188,8 +196,9 @@ jobs: # subject to that restriction (verified live the same day: an # App-token `gh pr create` against this exact repo succeeded, PR # authored by app/wyre-agent-fleet). Used only for the `gh pr` calls - # below — `git push` keeps using actions/checkout's default credential, - # which was never the blocked operation. + # below — `git push` authenticates separately with the default + # GITHUB_TOKEN via an inline http.extraheader (see PUSH_TOKEN below), + # since push was never the blocked operation. - name: "Prepare: mint App token for PR creation" if: steps.mode.outputs.mode == 'prepare' && steps.prepare.outputs.release_needed == 'true' id: app-token @@ -220,6 +229,7 @@ jobs: if: steps.mode.outputs.mode == 'prepare' && steps.prepare.outputs.release_needed == 'true' env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + PUSH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.prepare.outputs.version }} run: | set -euo pipefail @@ -235,7 +245,8 @@ jobs: merge method) triggers this workflow's PUBLISH mode, which tags, publishes to npm, and creates the GitHub release — nothing publishes until this merges." - git push --force origin release/next + AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" + git -c http.extraheader="$AUTH_HEADER" push --force origin release/next if gh pr view release/next --json state --jq .state 2>/dev/null | grep -q OPEN; then gh pr edit release/next --title "chore(release): ${VERSION}" From f55bf8e745b648498cb04b5c3823107254339845 Mon Sep 17 00:00:00 2001 From: Aaron Sachs Date: Fri, 4 Sep 2026 00:40:20 +0000 Subject: [PATCH 3/4] fix(release): mask the computed auth header (boss's catch) GitHub auto-masks GITHUB_TOKEN itself but not derived/transformed values like its base64 encoding. ::add-mask:: right after computing it, before any git command uses it, at all 3 call sites. --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa69938..9227b1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,6 +116,7 @@ jobs: run: | set -euo pipefail AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$AUTH_HEADER" git -c http.extraheader="$AUTH_HEADER" fetch --tags VERSION=$(jq -r '.version' package.json) PKG_NAME=$(jq -r '.name' package.json) @@ -151,6 +152,7 @@ jobs: set -euo pipefail VERSION="${{ steps.mode.outputs.version }}" AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$AUTH_HEADER" git tag "v${VERSION}" git -c http.extraheader="$AUTH_HEADER" push origin "v${VERSION}" @@ -246,6 +248,7 @@ jobs: publishes to npm, and creates the GitHub release — nothing publishes until this merges." AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" + echo "::add-mask::$AUTH_HEADER" git -c http.extraheader="$AUTH_HEADER" push --force origin release/next if gh pr view release/next --json state --jq .state 2>/dev/null | grep -q OPEN; then From 62aa8673f0e76f78d93f1c0bd4ac48dbf04fc987 Mon Sep 17 00:00:00 2001 From: Aaron Sachs Date: Fri, 4 Sep 2026 00:56:38 +0000 Subject: [PATCH 4/4] fix(release): use explicit https URL instead of the origin remote name CodeRabbit catch (CWE-319): if something upstream ever rewrote origin's URL to http://, using the remote name would send the Basic-auth header in cleartext. An explicit https:// URL can't be redirected that way. --- .github/workflows/release.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9227b1f..8fd13d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -108,6 +108,13 @@ jobs: # credential to that one command's process environment -- nothing # persists to a file. Every git network call in this workflow uses # this same inline pattern; none set the remote URL. + # + # Also (CodeRabbit, CWE-319): every such call targets an explicit + # https://github.com/... URL rather than the `origin` remote name -- + # if something upstream of this point ever rewrote origin's URL to an + # http:// scheme, using the remote name would silently send this + # Basic-auth header in cleartext. An explicit https:// URL can't be + # redirected that way. - name: Determine mode (publish vs prepare) id: mode env: @@ -117,7 +124,7 @@ jobs: set -euo pipefail AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" echo "::add-mask::$AUTH_HEADER" - git -c http.extraheader="$AUTH_HEADER" fetch --tags + git -c http.extraheader="$AUTH_HEADER" fetch "https://github.com/${{ github.repository }}.git" --tags VERSION=$(jq -r '.version' package.json) PKG_NAME=$(jq -r '.name' package.json) @@ -154,7 +161,7 @@ jobs: AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" echo "::add-mask::$AUTH_HEADER" git tag "v${VERSION}" - git -c http.extraheader="$AUTH_HEADER" push origin "v${VERSION}" + git -c http.extraheader="$AUTH_HEADER" push "https://github.com/${{ github.repository }}.git" "v${VERSION}" - name: "Publish: npm publish" if: steps.mode.outputs.mode == 'publish' && steps.mode.outputs.npm_published == 'false' @@ -249,7 +256,7 @@ jobs: publishes until this merges." AUTH_HEADER="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 | tr -d '\n')" echo "::add-mask::$AUTH_HEADER" - git -c http.extraheader="$AUTH_HEADER" push --force origin release/next + git -c http.extraheader="$AUTH_HEADER" push --force "https://github.com/${{ github.repository }}.git" release/next if gh pr view release/next --json state --jq .state 2>/dev/null | grep -q OPEN; then gh pr edit release/next --title "chore(release): ${VERSION}"