Skip to content

Commit 5d4a974

Browse files
authored
Merge pull request #11938 from sbidoul/fix-direct-url-hash-trusted-sbi
Don't trust link hash in direct URL dependencies
2 parents 62e932a + 453a5a7 commit 5d4a974

File tree

3 files changed

+122
-1
lines changed

3 files changed

+122
-1
lines changed

news/11938.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When package A depends on package B provided as a direct URL dependency including a hash
2+
embedded in the link, the ``--require-hashes`` option did not warn when user supplied hashes
3+
were missing for package B.

src/pip/_internal/req/req_install.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,12 @@ def hashes(self, trust_internet: bool = True) -> Hashes:
287287
288288
"""
289289
good_hashes = self.hash_options.copy()
290-
link = self.link if trust_internet else self.original_link
290+
if trust_internet:
291+
link = self.link
292+
elif self.original_link and self.user_supplied:
293+
link = self.original_link
294+
else:
295+
link = None
291296
if link and link.hash:
292297
good_hashes.setdefault(link.hash_name, []).append(link.hash)
293298
return Hashes(good_hashes)

tests/functional/test_install.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
import os
23
import re
34
import ssl
@@ -13,6 +14,7 @@
1314
from pip._internal.cli.status_codes import ERROR, SUCCESS
1415
from pip._internal.models.index import PyPI, TestPyPI
1516
from pip._internal.utils.misc import rmtree
17+
from pip._internal.utils.urls import path_to_url
1618
from tests.conftest import CertFactory
1719
from tests.lib import (
1820
PipTestEnvironment,
@@ -616,6 +618,117 @@ def test_hashed_install_failure(script: PipTestEnvironment, tmpdir: Path) -> Non
616618
assert len(result.files_created) == 0
617619

618620

621+
def test_link_hash_pass_require_hashes(
622+
script: PipTestEnvironment, shared_data: TestData
623+
) -> None:
624+
"""Test that a good hash in user provided direct URL is
625+
considered valid for --require-hashes."""
626+
url = path_to_url(str(shared_data.packages.joinpath("simple-1.0.tar.gz")))
627+
url = (
628+
f"{url}#sha256="
629+
"393043e672415891885c9a2a0929b1af95fb866d6ca016b42d2e6ce53619b653"
630+
)
631+
script.pip_install_local("--no-deps", "--require-hashes", url)
632+
633+
634+
def test_bad_link_hash_install_failure(
635+
script: PipTestEnvironment, shared_data: TestData
636+
) -> None:
637+
"""Test that wrong hash in direct URL stops installation."""
638+
url = path_to_url(str(shared_data.packages.joinpath("simple-1.0.tar.gz")))
639+
url = f"{url}#sha256=invalidhash"
640+
result = script.pip_install_local("--no-deps", url, expect_error=True)
641+
assert "THESE PACKAGES DO NOT MATCH THE HASHES" in result.stderr
642+
643+
644+
def test_bad_link_hash_good_user_hash_install_success(
645+
script: PipTestEnvironment, shared_data: TestData, tmp_path: Path
646+
) -> None:
647+
"""Test that wrong hash in direct URL ignored when good --hash provided.
648+
649+
This behaviour may be accidental?
650+
"""
651+
url = path_to_url(str(shared_data.packages.joinpath("simple-1.0.tar.gz")))
652+
url = f"{url}#sha256=invalidhash"
653+
digest = "393043e672415891885c9a2a0929b1af95fb866d6ca016b42d2e6ce53619b653"
654+
with requirements_file(
655+
f"simple @ {url} --hash sha256:{digest}", tmp_path
656+
) as reqs_file:
657+
script.pip_install_local("--no-deps", "--require-hashes", "-r", reqs_file)
658+
659+
660+
def test_link_hash_in_dep_fails_require_hashes(
661+
script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
662+
) -> None:
663+
"""Test that a good hash in direct URL dependency is not considered
664+
for --require-hashes."""
665+
# Create a project named pkga that depends on the simple-1.0.tar.gz with a direct
666+
# URL including a hash.
667+
simple_url = path_to_url(str(shared_data.packages.joinpath("simple-1.0.tar.gz")))
668+
simple_url_with_hash = (
669+
f"{simple_url}#sha256="
670+
"393043e672415891885c9a2a0929b1af95fb866d6ca016b42d2e6ce53619b653"
671+
)
672+
project_path = tmp_path / "pkga"
673+
project_path.mkdir()
674+
project_path.joinpath("pyproject.toml").write_text(
675+
textwrap.dedent(
676+
f"""\
677+
[project]
678+
name = "pkga"
679+
version = "1.0"
680+
dependencies = ["simple @ {simple_url_with_hash}"]
681+
"""
682+
)
683+
)
684+
# Build a wheel for pkga and compute its hash.
685+
wheelhouse = tmp_path / "wheehouse"
686+
wheelhouse.mkdir()
687+
script.pip("wheel", "--no-deps", "-w", wheelhouse, project_path)
688+
digest = hashlib.sha256(
689+
wheelhouse.joinpath("pkga-1.0-py3-none-any.whl").read_bytes()
690+
).hexdigest()
691+
# Install pkga from a requirements file with hash, using --require-hashes.
692+
# This should fail because we have not provided a hash for the 'simple' dependency.
693+
with requirements_file(f"pkga==1.0 --hash sha256:{digest}", tmp_path) as reqs_file:
694+
result = script.pip(
695+
"install",
696+
"--no-build-isolation",
697+
"--require-hashes",
698+
"--no-index",
699+
"-f",
700+
wheelhouse,
701+
"-r",
702+
reqs_file,
703+
expect_error=True,
704+
)
705+
assert "Hashes are required in --require-hashes mode" in result.stderr
706+
707+
708+
def test_bad_link_hash_in_dep_install_failure(
709+
script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
710+
) -> None:
711+
"""Test that wrong hash in direct URL dependency stops installation."""
712+
url = path_to_url(str(shared_data.packages.joinpath("simple-1.0.tar.gz")))
713+
url = f"{url}#sha256=invalidhash"
714+
project_path = tmp_path / "pkga"
715+
project_path.mkdir()
716+
project_path.joinpath("pyproject.toml").write_text(
717+
textwrap.dedent(
718+
f"""\
719+
[project]
720+
name = "pkga"
721+
version = "1.0"
722+
dependencies = ["simple @ {url}"]
723+
"""
724+
)
725+
)
726+
result = script.pip_install_local(
727+
"--no-build-isolation", project_path, expect_error=True
728+
)
729+
assert "THESE PACKAGES DO NOT MATCH THE HASHES" in result.stderr, result.stderr
730+
731+
619732
def assert_re_match(pattern: str, text: str) -> None:
620733
assert re.search(pattern, text), f"Could not find {pattern!r} in {text!r}"
621734

0 commit comments

Comments
 (0)