#14315·pip

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

Author: yonnnxrCreated Sep 15, 2026Updated Sep 15, 2026

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

python
>>> 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 suggest this is not intended:

  1. _clean_link(), which decides whether two links are equivalent, reads only the fragment (urllib.parse.parse_qs(parsed.fragment)). The same URL therefore counts 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.

Consequence: 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 with the CLI against a locally built wheel (no network):

bash
$ cat req_query.txt
pkg @ file:///tmp/pkg-1.0.0-py3-none-any.whl?token=x&sha256=<digest>

$ python -m pip install --dry-run --no-index --no-deps --require-hashes -r req_query.txt
Processing /tmp/pkg-1.0.0-py3-none-any.whl
Would install pkg-1.0.0

$ cat req_nohash.txt
pkg @ file:///tmp/pkg-1.0.0-py3-none-any.whl

$ 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.

Per SECURITY.md this looks like a normal parser bug rather than a security report, so I'm filing it here.

Is this divergence worth fixing? A patch that searches only urlsplit(url).fragment keeps every documented form working, including #subdirectory=setup&sha256=..., and makes the parser agree with _clean_link. Happy to open a PR if so.