sync-upstream: confirm the repository behind a release feed - #255
Open
firemanxbr wants to merge 1 commit into
Open
sync-upstream: confirm the repository behind a release feed#255firemanxbr wants to merge 1 commit into
firemanxbr wants to merge 1 commit into
Conversation
upstream.github is metadata a person writes once, in a pull request, and has no reason to revisit. Two things change underneath it without anything in the sync noticing. A renamed or transferred repository keeps resolving through GitHub's redirect, so every sync keeps passing while the declared name sits abandoned and free for anyone to register; whoever takes it inherits the package's release feed. Comparing the full_name GitHub returns against what is declared is the whole defense and costs one field. An archived repository cannot publish anything, so a feed that appears to ship a new release from one is not the feed we believe we are reading -- the same class of anomaly this provider already treats as a loud error. A repository response that cannot be read fails too, on the same grounds an unusable tag does. Repository age and release count are deliberately not checked. Neither is evidence about an artifact: a project can be new and sound, or years old and never once reviewed, and an attacker willing to wait defeats any threshold on either while sound young upstreams get rejected. Every check here reports a fact about the repository as it is now, which is the only kind of claim this can honestly make. The call sits on the path that already fetches a checksum manifest, so a scheduled run over every package pays for it only when a release is actually about to be adopted. Adds 8 self-test cases. The fetch sits behind a function as the existing ones do, so the path stays offline-testable; an unset fixture reports the declared repository live and unarchived, leaving existing cases untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
upstream.githubnames the repository a package's releases come from. It iswritten once, in the pull request that adds the package, and there is no reason
for anyone to look at it again.
Two things change underneath it without anything in the sync noticing.
A repository gets renamed or transferred. GitHub keeps serving the old
owner/repothrough a redirect, so the feed still resolves and every sync keepspassing. Meanwhile the old name is released back into the namespace: anyone can
register an account under the old owner and recreate the repository, and from
that point the redirect stops and
sync-upstreamreads their releases instead.The package keeps building, keeps getting signed, and nothing in the pipeline has
a reason to complain.
A repository gets archived. An archived repository cannot publish a release,
so a feed that appears to ship one from it is not the feed we think we are
reading — the same class of anomaly this provider already treats as a loud error
when a tag or a timestamp is unusable.
Both are one field away from being detectable, so this checks them:
full_nameGitHub returns must match whatupstream.githubdeclares(case-insensitively, since GitHub does not distinguish case)
an unusable tag does
What this deliberately does not check
Repository age and release count. Neither is evidence about an artifact — a
project can be new and sound, or years old and never once reviewed — and a
threshold on either rejects sound young upstreams while stopping no attacker who
is willing to wait. Every check here reports a fact about the repository as it is
right now, which is the only kind of claim it can honestly make.
Cost
One
GET /repos/:owner/:repo, on the path that already fetches the checksummanifest, so it runs only when a release is actually about to be adopted rather
than once per package on a scheduled sync.
Testing
bin/sync-upstream self-testgains 8 cases covering the rename, the archive, aresponse missing
archived, an unreadable response, the case-insensitivity rule,and an end-to-end run where the release feed is impeccable and the repository
behind it is the thing that is wrong. The fetch sits behind a function like the
existing two, so the path stays offline; an unset fixture reports the declared
repository live and unarchived, which leaves every existing case unchanged.
48 assertions pass, verified on Arch with bash 5.3.8 and pacman's
vercmp.Known limitation
The check runs when a new release is adopted, which means a repository that is
renamed while its feed is dormant is not noticed until the next release. Covering
that needs a periodic re-verification of every package rather than a check on the
adoption path, which seemed like a separate change rather than something to fold
in here. Happy to follow up with it if you want it.