PoC: binary xcframework release pipeline#439
Conversation
| run: | | ||
| set -euo pipefail | ||
|
|
||
| VERSION="${{ inputs.version }}" | ||
| DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${VERSION}/${{ env.ARTIFACT_NAME }}" | ||
|
|
||
| echo "Binary target URL: $DOWNLOAD_URL" | ||
| echo "Binary target checksum: ${{ env.ARTIFACT_DIGEST }}" | ||
| echo "" | ||
| echo "PLACEHOLDER: Update Package.swift to include:" | ||
| echo "" | ||
| echo " .binaryTarget(" | ||
| echo " name: \"Segment\"," | ||
| echo " url: \"$DOWNLOAD_URL\"," | ||
| echo " checksum: \"${{ env.ARTIFACT_DIGEST }}\"" | ||
| echo " )" | ||
| echo "" | ||
| echo "NOTE: This requires a commit to the repo after the release is created." | ||
| echo "The manifest update is a separate commit that references the released artifact." |
There was a problem hiding this comment.
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".
🌟 Fixed in commit 1ddac56 🌟
| run: | | ||
| set -euo pipefail | ||
|
|
||
| VERSION="${{ inputs.version }}" | ||
|
|
||
| gh release create "$VERSION" \ | ||
| "${{ env.ARTIFACT_NAME }}" \ | ||
| --title "Version $VERSION" \ | ||
| --notes "$(cat <<'EOF' | ||
| ## Segment.xcframework $VERSION | ||
|
|
||
| Binary Swift package release. | ||
|
|
||
| ### Consumer verification | ||
|
|
||
| Verify the build attestation (build-from-commit proof): | ||
| ``` | ||
| gh attestation verify Segment.xcframework.zip -R ${{ github.repository }} --signer-workflow ${{ env.BUILD_WORKFLOW_REF }} | ||
| ``` | ||
|
|
||
| ### Checksum (for Package.swift binaryTarget) | ||
| ``` | ||
| ${{ env.ARTIFACT_DIGEST }} | ||
| ``` | ||
| EOF | ||
| )" | ||
|
|
||
| echo "Published ${{ env.ARTIFACT_NAME }} to release $VERSION" | ||
|
|
There was a problem hiding this comment.
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".
🥳 Fixed in commit 1ddac56 🥳
| run: | | ||
| set -euo pipefail | ||
|
|
||
| COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}') | ||
| EXPECTED_DIGEST="${{ inputs.artifact-digest }}" | ||
|
|
||
| echo "Computed digest: $COMPUTED_DIGEST" | ||
| echo "Expected digest: $EXPECTED_DIGEST" | ||
|
|
||
| if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then | ||
| echo "::error::DIGEST MISMATCH — artifact may have been tampered with" | ||
| echo "::error::Expected: $EXPECTED_DIGEST" | ||
| echo "::error::Got: $COMPUTED_DIGEST" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Digest verified: $COMPUTED_DIGEST" | ||
| echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV" | ||
|
|
There was a problem hiding this comment.
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".
🎉 Fixed in commit 1ddac56 🎉
…, publish-public) Implements the ADR's five-stage model for distributing analytics-swift as a binary Swift package. The build produces an xcframework with a consumer-verifiable build attestation, and the publish workflow gates on internal clearance before uploading to GitHub Releases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add OIDC token exchange (github-actions-segmentio provider) to get a short-lived Artifactory read token - Configure SPM mirrors to resolve through virtual-swift-thirdparty - Pin all actions to commit SHAs, use macos-26 runner - Rename release-build.yml to build.yml - Clean up publish.yml placeholders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3692453 to
1ddac56
Compare
| run: | | ||
| set -euo pipefail | ||
| VERSION="${{ inputs.version }}" | ||
|
|
||
| gh release create "$VERSION" \ | ||
| "${{ env.ARTIFACT_NAME }}" \ | ||
| --title "Version $VERSION" \ | ||
| --notes "## Segment.xcframework ${VERSION} | ||
|
|
||
| Binary Swift package release. | ||
|
|
||
| ### Consumer verification | ||
|
|
||
| Verify the build attestation: | ||
| \`\`\` | ||
| gh attestation verify Segment.xcframework.zip -R ${{ github.repository }} --signer-workflow ${{ env.BUILD_WORKFLOW_REF }} | ||
| \`\`\` | ||
|
|
||
| ### Checksum (for Package.swift binaryTarget) | ||
| \`\`\` | ||
| ${{ env.ARTIFACT_DIGEST }} | ||
| \`\`\`" |
There was a problem hiding this comment.
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".
🎉 Removed in commit b46b44a 🎉
| run: | | ||
| set -euo pipefail | ||
| COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}') | ||
| EXPECTED_DIGEST="${{ inputs.artifact-digest }}" | ||
|
|
||
| if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then | ||
| echo "::error::DIGEST MISMATCH — artifact may have been tampered with" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Digest verified: $COMPUTED_DIGEST" | ||
| echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV" | ||
|
|
There was a problem hiding this comment.
Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".
⭐ Removed in commit b46b44a ⭐
Change from tag-only trigger to push/PR/manual dispatch so we can validate Artifactory dependency resolution on every build. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The org policy only allows enterprise-owned or GitHub-created actions. Use xcode-select directly since macOS runners have Xcode pre-installed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
macos-26 has no available runners. Use macos-15 (GitHub-hosted standard) with Xcode 16.2 which ships on that image. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lows No macOS runners available in the org. Add a lightweight workflow that validates SPM dependency resolution through Artifactory via OIDC on ubuntu-latest. Move build.yml and publish.yml to workflows-disabled/ until macOS runners are available. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| run: | | ||
| set -euo pipefail | ||
| VERSION="${{ inputs.version }}" | ||
|
|
||
| gh release create "$VERSION" \ | ||
| "${{ env.ARTIFACT_NAME }}" \ | ||
| --title "Version $VERSION" \ | ||
| --notes "## Segment.xcframework ${VERSION} | ||
|
|
||
| Binary Swift package release. | ||
|
|
||
| ### Consumer verification | ||
|
|
||
| Verify the build attestation: | ||
| \`\`\` | ||
| gh attestation verify Segment.xcframework.zip -R ${{ github.repository }} --signer-workflow ${{ env.BUILD_WORKFLOW_REF }} | ||
| \`\`\` | ||
|
|
||
| ### Checksum (for Package.swift binaryTarget) | ||
| \`\`\` | ||
| ${{ env.ARTIFACT_DIGEST }} | ||
| \`\`\`" |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
Untrusted workflow input inputs.version is interpolated directly into shell command in a run: step, allowing arbitrary code execution. Attackers can inject shell metacharacters via workflow dispatch to execute malicious commands on the runner.
More details about this
The run: step directly interpolates ${{ inputs.version }} into a shell command without protection. An attacker could exploit this by submitting a workflow dispatch with a malicious version input containing shell metacharacters. For example, if an attacker provides version as 1.0.0"; rm -rf /; echo ", the shell would execute:
VERSION="1.0.0"; rm -rf /; echo ""
gh release create ...
This allows arbitrary command execution on the runner, potentially enabling theft of GITHUB_TOKEN and other repository secrets, code exfiltration, or runner compromise.
The vulnerable code stores the untrusted inputs.version directly into VERSION without escaping, then uses it unquoted in the gh release create command and in string interpolation for the release title. While env.ARTIFACT_DIGEST, env.ARTIFACT_NAME, and env.BUILD_WORKFLOW_REF are safe (they come from environment variables, not user input), the inputs.version variable poses a direct injection risk.
To resolve this comment:
✨ Commit fix suggestion
| run: | | |
| set -euo pipefail | |
| VERSION="${{ inputs.version }}" | |
| gh release create "$VERSION" \ | |
| "${{ env.ARTIFACT_NAME }}" \ | |
| --title "Version $VERSION" \ | |
| --notes "## Segment.xcframework ${VERSION} | |
| Binary Swift package release. | |
| ### Consumer verification | |
| Verify the build attestation: | |
| \`\`\` | |
| gh attestation verify Segment.xcframework.zip -R ${{ github.repository }} --signer-workflow ${{ env.BUILD_WORKFLOW_REF }} | |
| \`\`\` | |
| ### Checksum (for Package.swift binaryTarget) | |
| \`\`\` | |
| ${{ env.ARTIFACT_DIGEST }} | |
| \`\`\`" | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| REPOSITORY: ${{ github.repository }} | |
| ARTIFACT_NAME: ${{ env.ARTIFACT_NAME }} | |
| BUILD_WORKFLOW_REF: ${{ env.BUILD_WORKFLOW_REF }} | |
| ARTIFACT_DIGEST: ${{ env.ARTIFACT_DIGEST }} | |
| run: | | |
| set -euo pipefail | |
| gh release create "$VERSION" \ | |
| "$ARTIFACT_NAME" \ | |
| --title "Version $VERSION" \ | |
| --notes "## Segment.xcframework $VERSION | |
| Binary Swift package release. | |
| ### Consumer verification | |
| Verify the build attestation: | |
| \`\`\` | |
| gh attestation verify Segment.xcframework.zip -R $REPOSITORY --signer-workflow $BUILD_WORKFLOW_REF | |
| \`\`\` | |
| ### Checksum (for Package.swift binaryTarget) | |
| \`\`\` | |
| $ARTIFACT_DIGEST | |
| \`\`\`" |
View step-by-step instructions
-
Move the untrusted workflow input out of the shell script and into the step
env:block. For example, setVERSION: ${{ inputs.version }}on theCreate GitHub Releasestep instead of assigningVERSION="${{ inputs.version }}"insiderun:. -
Remove the direct
${{ inputs.version }}interpolation from therun:script and use the environment variable instead. Keep all shell references quoted, such as"$VERSION". -
If you need the repository value inside the release notes text, pass it through
env:as well and reference it from the shell, for exampleREPOSITORY: ${{ github.repository }}and then use"$REPOSITORY"in the command text. This avoids mixing GitHub expression expansion directly into a shell script. -
Update the
gh release createcommand so the shell only reads environment variables insiderun:. For example, use values like"$VERSION","$ARTIFACT_NAME","$BUILD_WORKFLOW_REF", and"$ARTIFACT_DIGEST"in the command arguments and notes body. -
Keep the multi-line
--notesargument as a shell string that expands only environment variables, not${{ ... }}expressions. This prevents user-controlled workflow input from being interpreted by the runner before the shell handles quoting.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.
Need help with this issue? Consult our appsec team or ask in #help-appsec on Slack.
You can view more details about this finding in the Semgrep AppSec Platform.
| run: | | ||
| set -euo pipefail | ||
| COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}') | ||
| EXPECTED_DIGEST="${{ inputs.artifact-digest }}" | ||
|
|
||
| if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then | ||
| echo "::error::DIGEST MISMATCH — artifact may have been tampered with" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Digest verified: $COMPUTED_DIGEST" | ||
| echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV" | ||
|
|
There was a problem hiding this comment.
Semgrep identified an issue in your code:
User-controlled workflow input inputs.artifact-digest is directly interpolated into a shell script, allowing command injection. An attacker could inject arbitrary commands to steal secrets or compromise the workflow.
More details about this
The run: step directly interpolates ${{ inputs.artifact-digest }} into the shell script without any sanitization. Since inputs.artifact-digest is passed from user input through the workflow input parameter, an attacker could inject arbitrary shell commands into the EXPECTED_DIGEST variable.
Exploit scenario:
- An attacker submits a workflow dispatch request with
artifact-digestset to something like"; rm -rf /; # - This malicious value gets interpolated directly into the script:
EXPECTED_DIGEST="${{ inputs.artifact-digest }}"becomesEXPECTED_DIGEST=""; rm -rf /; #" - The shell interprets the injected commands and executes them with the runner's full permissions
- The attacker can now steal secrets from
$GITHUB_TOKEN, exfiltrate the repository code, or modify the artifact before publishing
The comparison logic in the conditional doesn't execute unless the attacker's injected code is valid shell syntax, but they can still inject commands before it completes or use shell metacharacters to break out of the string and execute arbitrary code.
To resolve this comment:
✨ Commit fix suggestion
| run: | | |
| set -euo pipefail | |
| COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}') | |
| EXPECTED_DIGEST="${{ inputs.artifact-digest }}" | |
| if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then | |
| echo "::error::DIGEST MISMATCH — artifact may have been tampered with" | |
| exit 1 | |
| fi | |
| echo "Digest verified: $COMPUTED_DIGEST" | |
| echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV" | |
| env: | |
| EXPECTED_DIGEST: ${{ inputs.artifact-digest }} | |
| run: | | |
| set -euo pipefail | |
| COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}') | |
| if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then | |
| echo "::error::DIGEST MISMATCH — artifact may have been tampered with" | |
| exit 1 | |
| fi | |
| echo "Digest verified: $COMPUTED_DIGEST" | |
| echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV" |
View step-by-step instructions
-
Move the untrusted workflow input out of the shell script and into the step
env:block.
Add something likeenv: EXPECTED_DIGEST: ${{ inputs.artifact-digest }}on theVerify artifact digeststep. -
Stop interpolating
${{ inputs.artifact-digest }}insiderun:.
ReplaceEXPECTED_DIGEST="${{ inputs.artifact-digest }}"withEXPECTED_DIGEST="$EXPECTED_DIGEST"or compare directly against"$EXPECTED_DIGEST"in theifstatement. -
Use the environment variable with double quotes everywhere it is read in the shell.
Keep the comparison in the formif [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then ... fiso the value is treated as data, not shell syntax. -
Apply the same pattern to other
run:steps that interpolate${{ ... }}values frominputsorgithubcontext.
For example, move values such as${{ inputs.version }},${{ github.repository }}, and${{ github.sha }}into step-levelenv:entries, then reference them as"$VERSION","$GITHUB_REPOSITORY", or"$GITHUB_SHA"inside the script.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.
Need help with this issue? Consult our appsec team or ask in #help-appsec on Slack.
You can view more details about this finding in the Semgrep AppSec Platform.
SPM/git on Linux wasn't reading ~/.netrc for auth. Use git credential store with the token embedded in ~/.git-credentials instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use git url.insteadOf to embed token in all Artifactory URLs (more reliable than credential store or .netrc) - Add token verification step to catch OIDC issues early - Limit push trigger to poc/** branches to avoid double-trigger Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Swift virtual repo expects a bearer credential per the docs. Use git http.extraHeader to pass Authorization: Bearer instead of basic-auth credentials. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
.xcframework)release-build.yml(build + attest) andpublish.yml(verify clearance + publish to GitHub Release)How it works
actions/attest-build-provenance, uploads artifactKnown gaps
Package.swiftbinary target update after release requires a separate commitTest plan
actions/attest-build-provenanceemits attestation for the zip digestgh attestation verifyworks for consumers against the build attestation