Skip to content

Commit c9065db

Browse files
committed
Fix pypa#5874: Filter out candidates with hash mismatches.
Changes the behavior of `pip install` in hash-checking mode to filter out any candidates whose hashes (obtained via URL) do not match the hashes provided. This prevents a HashMismatch error when a more preferred binary distribution is upload for a release after a user pins the hashes for that release. Note that a second hash comparison is performed when the candidate is downloaded. This is important because the first check is not secure: it trusts that the hash in the URL is the same as the hash in the content, and it also does not error when the user is in hash-checking mode but has not provided a hash.
1 parent f8732ac commit c9065db

File tree

3 files changed

+28
-3
lines changed

3 files changed

+28
-3
lines changed

news/5874.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Only select candidates in hash-checking mode that will pass hash checks.

src/pip/_internal/index.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from pip._internal.pep425tags import Pep425Tag
5252
from pip._internal.req import InstallRequirement
5353
from pip._internal.download import PipSession
54+
from pip._internal.utils import Hashes
5455

5556
SecureOrigin = Tuple[str, str, Optional[str]]
5657
BuildTag = Tuple[Any, ...] # either empty tuple or Tuple[int, str]
@@ -386,12 +387,28 @@ def iter_applicable(self):
386387
# Again, converting version to str to deal with debundling.
387388
return (c for c in self.iter_all() if str(c.version) in self._versions)
388389

389-
def get_best(self):
390-
# type: () -> Optional[InstallationCandidate]
390+
def get_best(self, hashes=None):
391+
# type: (Optional[Hashes]) -> Optional[InstallationCandidate]
391392
"""Return the best candidate available, or None if no applicable
392393
candidates are found.
393394
"""
394395
candidates = list(self.iter_applicable())
396+
if hashes:
397+
# If we are in hash-checking mode, filter out candidates that will
398+
# fail the hash check. This prevents HashMismatch errors when a new
399+
# distribution is uploaded for an old release.
400+
def test_against_hashes(candidate):
401+
link = candidate.location
402+
is_match = hashes.test_against_hash(link.hash_name, link.hash)
403+
if not is_match:
404+
logger.warning(
405+
"candidate %s ignored: hash %s:%s not among provided "
406+
"hashes",
407+
link.filename, link.hash_name, link.hash,
408+
)
409+
return is_match
410+
411+
candidates = [c for c in candidates if test_against_hashes(c)]
395412
return self._evaluator.get_best_candidate(candidates)
396413

397414

@@ -764,7 +781,9 @@ def find_requirement(self, req, upgrade):
764781
Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise
765782
"""
766783
candidates = self.find_candidates(req.name, req.specifier)
767-
best_candidate = candidates.get_best()
784+
# Get any hashes supplied by the user to filter candidates.
785+
hashes = req.hashes(trust_internet=False)
786+
best_candidate = candidates.get_best(hashes)
768787

769788
installed_version = None # type: Optional[_BaseVersion]
770789
if req.satisfied_by is not None:

src/pip/_internal/utils/hashes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ def check_against_path(self, path):
8686
with open(path, 'rb') as file:
8787
return self.check_against_file(file)
8888

89+
def test_against_hash(self, hash_name, hash_value):
90+
# type (str, str) -> bool
91+
"""Return whether a given hash is among the known-good hashes."""
92+
return hash_value in self._allowed.get(hash_name, [])
93+
8994
def __nonzero__(self):
9095
# type: () -> bool
9196
"""Return whether I know any known-good hashes."""

0 commit comments

Comments
 (0)