Skip to content

Read link hashes only from the URL fragment - #14316

Open
yonnnxr wants to merge 1 commit into
pypa:mainfrom
yonnnxr:fix-link-hash-query
Open

yonnnxr wants to merge 1 commit into
pypa:mainfrom
yonnnxr:fix-link-hash-query

Conversation

@yonnnxr

@yonnnxr yonnnxr commented Sep 15, 2026

Copy link
Copy Markdown

What does this PR do?

Fixes #14315.

LinkHash.find_hash_url_fragment() regex-searched the whole URL for
[#&](algo)=value, so a query-string parameter named after a hash algorithm was
read as a pinned hash:

>>> from pip._internal.models.link import Link
>>> Link("https://example.com/pkg.whl?token=x&sha256=" + "a" * 64).has_hash
True

This only triggers when the parameter is not the first one (?sha256=... alone
does not match, since the separator has to be # or &).

Two things make this a divergence rather than a feature:

  1. _clean_link(), used to decide whether two links are equivalent, reads only
    the fragment (urllib.parse.parse_qs(parsed.fragment)). The same URL
    therefore counted as hashed for pinning and as unhashed for equivalence.
  2. The regex is named _hash_url_fragment_re; news/11936.bugfix.rst describes
    the change that introduced [#&] as "Fix and improve the parsing of hashes
    embedded in URL fragments"
    ; and every case in test_link_hash_parsing uses
    #. The & alternative covers &-separated parameters inside the fragment
    (#subdirectory=setup&sha256=...), not a query.

For a direct, user-supplied requirement,
InstallRequirement.hashes(trust_internet=False) includes the link's hash, so a
query value counts towards --require-hashes. Reproduced against a locally
built wheel (no network):

$ python -m pip install --dry-run --no-index --no-deps --require-hashes -r req_query.txt
Would install pkg-1.0.0

$ python -m pip install --dry-run --no-index --no-deps --require-hashes -r req_nohash.txt
ERROR: Hashes are required in --require-hashes mode, but they are missing from some requirements.

How

  • LinkHash._hash_url_fragment_re anchors the algorithm name to the start of the
    fragment or a preceding & ((?:^|&)) instead of [#&].
  • find_hash_url_fragment() searches urlsplit(url).fragment instead of the
    whole URL.

Hex/length validation is deliberately not added: the existing comment
explains why malformed digests are handled by Hashes, and that is unchanged.

This is not a change of intent

The scope was already the fragment; matching a query parameter was a side effect
of running the regex over the whole URL. See the issue for the history.

Behaviour

URL before after
...#sha256=<digest> hash hash
...#sha256=<digest>&subdirectory=x hash hash
...#subdirectory=x&sha256=<digest> hash hash
...?token=x&sha256=<digest> hash none
...?sha256=<digest> none none

Tests

Two tests added to tests/unit/test_link.py:

  • test_hash_is_read_only_from_fragment — a query parameter (second and first
    position) and a path segment are not hashes. Fails on main before the change.
  • test_hash_in_fragment_after_other_params — the fragment form still works
    after other fragment parameters.

test_link_hash_parsing in tests/unit/test_collector.py is unaffected and
passes with the patch applied.

PR Checklist:

  • I agree to follow the [PSF Code of Conduct].
  • I have read and have followed the [CONTRIBUTING.md] file.
  • I have added a news file fragment (or this PR does not need one).
  • I have read and followed the [AI_POLICY.md] file, and if any AI tools were used, I have disclosed it below.

Assisted-by: Claude

LinkHash.find_hash_url_fragment() searched the whole URL for
[#&](algo)=value, so a query-string parameter named after a hash
algorithm was treated as a pinned hash. _clean_link() already reads only
the fragment, so the same URL counted as hashed for pinning and as
unhashed for link equivalence.

Search urlsplit(url).fragment instead, anchoring the algorithm name to
the start of the fragment or a preceding &. Every documented form keeps
working, including #subdirectory=setup&sha256=...

Refs pypa#14315
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Link hashes are parsed from the query string, not only the URL fragment

1 participant