Summary
The .github/workflows/publishing2PyPI.yml workflow installs build and twine without pinning to exact versions. Because these tools run at release time and twine uploads directly to PyPI using a production API token, an unpinned install creates a supply chain attack window: a malicious version of either package could be published to PyPI, picked up automatically during the next SDK release, and used to exfiltrate the PYPI_API_TOKEN or inject malicious code into the sift-python package itself.
Since sift-python is a publicly distributed SDK installed by Sift customers in their production environments, a compromised release would affect every downstream consumer.
Current vulnerable lines:
# publishing2PyPI.yml:30
python -m pip install build # ← unpinned
# publishing2PyPI.yml:37
python -m pip install --user --upgrade twine # ← unpinned + --upgrade
Remediation
Step 1 — Determine current known-good versions
Run locally to get exact versions:
pip install build twine
pip show build twine
# note the Version: fields
As of mid-2025, the latest stable versions are:
build → 1.2.2.post1
twine → 6.1.0
Step 2 — Generate hashes for pinned deps
pip download build==1.2.2.post1 twine==6.1.0 --no-deps -d /tmp/pkg
sha256sum /tmp/pkg/*.whl /tmp/pkg/*.tar.gz
Or use pip-compile with hashes:
pip install pip-tools
echo -e "build==1.2.2.post1\ntwine==6.1.0" > publish-requirements.in
pip-compile --generate-hashes --output-file publish-requirements.txt publish-requirements.in
Step 3 — Create publish-requirements.txt
Commit a publish-requirements.txt at the repo root containing the pinned, hashed dependencies. Example structure (replace hashes with actual output from step 2):
# Generated by pip-compile. DO NOT EDIT MANUALLY.
build==1.2.2.post1 \
--hash=sha256:<hash-of-wheel> \
--hash=sha256:<hash-of-sdist>
twine==6.1.0 \
--hash=sha256:<hash-of-wheel> \
--hash=sha256:<hash-of-sdist>
# ...transitive deps with hashes...
Step 4 — Update publishing2PyPI.yml
Replace the two unpinned install steps:
# BEFORE
- name: Create distribution files
run: |
python -m pip install build
python -m build
- name: Upload distribution files
env:
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
TWINE_USER: ${{ secrets.USER }}
run: |
python -m pip install --user --upgrade twine
ls dist/ | xargs -I % python -m twine upload --repository pypi dist/%
# AFTER
- name: Install publish dependencies
run: |
python -m pip install --require-hashes -r publish-requirements.txt
- name: Create distribution files
run: |
python -m build
- name: Upload distribution files
env:
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
TWINE_USER: ${{ secrets.USER }}
run: |
ls dist/ | xargs -I % python -m twine upload --repository pypi dist/%
Step 5 — Pin the GitHub Actions themselves
The workflow also uses mutable action tags rather than commit SHAs:
# BEFORE
uses: actions/checkout@v3
# AFTER — pin to a specific commit SHA
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Find the current SHA for each action at https://github.com/actions/<name>/tags.
Additional hardening (recommended)
Use PyPI's Trusted Publishers (OIDC) — eliminates the long-lived PYPI_API_TOKEN entirely. With Trusted Publishers, PyPI issues a short-lived token scoped to this specific workflow run. No secret to steal means the twine supply chain attack has no useful credential to exfiltrate.
Setup: https://docs.pypi.org/trusted-publishers/adding-a-publisher/
The updated workflow would look like:
permissions:
id-token: write # required for OIDC
contents: read
- name: Upload distribution files
uses: pypa/gh-action-pypi-publish@release/v1
# No TWINE_PASSWORD needed — OIDC token is issued automatically
Priority
🟠 P2 — This sprint. The publish workflow runs on every release. A compromised build or twine version published to PyPI would be picked up on the next sift-python release with no human intervention required, and would affect all customers who install the SDK.
References
Summary
The
.github/workflows/publishing2PyPI.ymlworkflow installsbuildandtwinewithout pinning to exact versions. Because these tools run at release time andtwineuploads directly to PyPI using a production API token, an unpinned install creates a supply chain attack window: a malicious version of either package could be published to PyPI, picked up automatically during the next SDK release, and used to exfiltrate thePYPI_API_TOKENor inject malicious code into thesift-pythonpackage itself.Since
sift-pythonis a publicly distributed SDK installed by Sift customers in their production environments, a compromised release would affect every downstream consumer.Current vulnerable lines:
Remediation
Step 1 — Determine current known-good versions
Run locally to get exact versions:
pip install build twine pip show build twine # note the Version: fieldsAs of mid-2025, the latest stable versions are:
build→1.2.2.post1twine→6.1.0Step 2 — Generate hashes for pinned deps
Or use
pip-compilewith hashes:Step 3 — Create
publish-requirements.txtCommit a
publish-requirements.txtat the repo root containing the pinned, hashed dependencies. Example structure (replace hashes with actual output from step 2):Step 4 — Update
publishing2PyPI.ymlReplace the two unpinned install steps:
Step 5 — Pin the GitHub Actions themselves
The workflow also uses mutable action tags rather than commit SHAs:
Find the current SHA for each action at
https://github.com/actions/<name>/tags.Additional hardening (recommended)
Use PyPI's Trusted Publishers (OIDC) — eliminates the long-lived
PYPI_API_TOKENentirely. With Trusted Publishers, PyPI issues a short-lived token scoped to this specific workflow run. No secret to steal means thetwinesupply chain attack has no useful credential to exfiltrate.Setup: https://docs.pypi.org/trusted-publishers/adding-a-publisher/
The updated workflow would look like:
Priority
🟠 P2 — This sprint. The publish workflow runs on every release. A compromised
buildortwineversion published to PyPI would be picked up on the nextsift-pythonrelease with no human intervention required, and would affect all customers who install the SDK.References
--require-hashesdocumentationgithubactions:S8544(unlocked dependency versions),githubactions:S8541(missing--only-binary)