Security guide
How the action touches the network and your tokens, what pull requests from forks can and cannot do, and how to lock down runs on untrusted input.
What the action actually does
- Reads dependency URLs from your
.xcodeprojorPackage.swiftmanifests in the checked-out workspace. - Contacts each dependency's git host with
git ls-remoteto list version tags and branch heads. The GitHub token is not used for these lookups. - Uses
github-token(default: the job's automatic${{ github.token }}) only to post, update, or delete the PR comment and — withopen-tracking-issue: true— the tracking issue.
URLs with embedded credentials are redacted before they appear in logs, outputs, or the PR comment.
Threat model: pull requests from forks
On your own branches, contacting the hosts named in your manifests is routine. A pull request from a fork, however, can rewrite those URLs, so treat fork runs as untrusted:
- Fork PRs get a read-only
GITHUB_TOKEN, so the comment step can't write. To run on fork PRs, trigger withpull_request_target(review the security implications) or post the results from a separate, trusted workflow. - Fork PR manifests are untrusted input. With
pull_request_target, checking out the PR head lets a fork changePackage.swiftor.xcodeprojdependency URLs. Because lookups usegit ls-remote, a malicious PR could point the runner at hosts it can reach and at credentials already available to git.
Transport restrictions
Version lookups run git non-interactively (GIT_TERMINAL_PROMPT=0, so a
credential prompt fails instead of hanging) and limit it to the https,
ssh, and git transports via GIT_ALLOW_PROTOCOL. The file and ext
transports and remote helpers are blocked, so a manifest URL cannot read local
files or execute helper programs on the runner. Lookups are retried a bounded
number of times and then fail the run — an unreachable dependency is never
silently treated as "no updates".
Restricting hosts with allow-hosts
allow-hosts is the main control for untrusted runs: it limits which hosts
git ls-remote may contact at all.
with:
package-manifest-paths: Modules/Package.swift
allow-hosts: github.com,gitlab.example.com
Matching semantics:
- Hostnames match exactly and case-insensitively —
github.comdoes not allowevil-github.comorsub.github.com. - Schemes, credentials, paths, and ports are ignored, so
https://github.com/foo/bar,git@github.com:foo/bar.git, andssh://git@github.com:2222/foo/barall matchgithub.com. - An empty
allow-hosts(the default) allows any host reachable over the allowed transports.
When a dependency's host is not on the list, the action fails before
contacting git and writes blocked=true plus a descriptive
error-message output naming the dependency and host.
Hardened fork-PR workflow
on:
pull_request_target:
permissions:
contents: read
pull-requests: write
jobs:
spm-version-updates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: hbmartin/github-action-spm_version_updates@v1
with:
package-manifest-paths: Modules/Package.swift
allow-hosts: github.com
Checklist for untrusted runs
- [ ] Check out the PR head with
persist-credentials: falseso the workflow's token is not left in the git credential store. - [ ] Set
allow-hoststo the host(s) your real dependencies live on. - [ ] Don't expose extra secrets, SSH keys, or private-network access to the job — anything git can already authenticate with, a rewritten manifest URL can point at.
- [ ] Keep
permissionsminimal:contents: readpluspull-requests: write(andissues: writeonly withopen-tracking-issue: true).