Description
Currently, we're quite strict about what external dependencies stubs can use. We have a small allowlist here:
stub_uploader/stub_uploader/metadata.py
Line 166 in b81ba3c
Briefly, the reason for this is a) Python packages can execute arbitrary code at install time, b) type checkers sometimes install stub packages automatically, c) stub packages are quite popular, d) users likely expect stub packages to be inert
So what would it take to remove the allowlist? We already have an important additional check in stub uploader: we ensure that external dependencies of a stub package must be a dependency of the upstream package. However, there's still a hole, in that stub dependencies of stub packages are not currently checked.
To spell things out, this is the scenario that we're concerned about:
- Package foo exists, that depends on bar
- typeshed adds types-foo, add dependency on bar
- In order to make types-foo useful, we need bar, so we do some security review of bar / the bar maintainer and add it to the allowlist
- bar (gets hacked and) adds a dependency on evil
- types-foo users are affected, while this isn't good we're okay with it, since those are assumed to be a subset of foo users
- Someone sneaks past typeshed maintainers and adds a dependency on types-foo to types-requests (wouldn't be too hard to do)
- types-requests users are affected, this is really bad
Once we plug this hole, we could maybe get rid of the allowlist or have much more relaxed criteria.
This has been discussed in a couple places, mainly #61 (comment). I'm writing this up here as a way to easily communicate the current status quo to typeshed contributors.
Plugging the hole is a pretty easy change to make to stub_uploader, see diff here:
diff --git a/stub_uploader/metadata.py b/stub_uploader/metadata.py
index 85d0643..56c5448 100644
--- a/stub_uploader/metadata.py
+++ b/stub_uploader/metadata.py
@@ -61,7 +61,7 @@ class Metadata:
def requires_typeshed(self) -> list[Requirement]:
reqs = self._unvalidated_requires_typeshed
for req in reqs:
- verify_typeshed_req(req)
+ verify_typeshed_req(req, self.upstream_distribution)
return reqs
@property
@@ -145,7 +145,7 @@ def strip_types_prefix(dependency: str) -> str:
return dependency.removeprefix(TYPES_PREFIX)
-def verify_typeshed_req(req: Requirement) -> None:
+def verify_typeshed_req(req: Requirement, upstream_distribution: Optional[str]) -> None:
if not req.name.startswith(TYPES_PREFIX):
raise InvalidRequires(f"Expected dependency {req} to start with {TYPES_PREFIX}")
@@ -154,9 +154,26 @@ def verify_typeshed_req(req: Requirement) -> None:
f"Expected dependency {req} to be uploaded from stub_uploader"
)
- # TODO: make sure that if a typeshed distribution depends on other typeshed stubs,
- # the upstream depends on the upstreams corresponding to those stubs.
- # See https://github.com/typeshed-internal/stub_uploader/pull/61#discussion_r979327370
+ if upstream_distribution is None:
+ raise InvalidRequires(
+ f"There is no upstream distribution on PyPI, so cannot verify {req}"
+ )
+
+ resp = requests.get(f"https://pypi.org/pypi/{upstream_distribution}/json")
+ if resp.status_code != 200:
+ raise InvalidRequires(
+ f"Expected dependency {req} to be accessible on PyPI, but got {resp.status_code}"
+ )
+
+ data = resp.json()
+
+ if strip_types_prefix(req.name) not in [
+ Requirement(r).name for r in (data["info"].get("requires_dist") or [])
+ ]:
+ raise InvalidRequires(
+ f"Expected dependency {strip_types_prefix(req.name)} to be listed in "
+ f"{upstream_distribution}'s requires_dist"
+ )
However, typeshed currently has twelve stubs that fail this test:
test_recursive_verify[Pygments] - stub_uploader.metadata.InvalidRequires: Expected dependency docutils to be listed in Pygments's requires_dist
test_recursive_verify[redis] - stub_uploader.metadata.InvalidRequires: Expected dependency pyOpenSSL to be listed in redis's requires_dist
test_recursive_verify[PyScreeze] - stub_uploader.metadata.InvalidRequires: Expected dependency Pillow to be listed in PyScreeze's requires_dist
test_recursive_verify[pyinstaller] - stub_uploader.metadata.InvalidRequires: Expected dependency docutils to be listed in setuptools's requires_dist
test_recursive_verify[pysftp] - stub_uploader.metadata.InvalidRequires: Expected dependency paramiko to be listed in pysftp's requires_dist
test_recursive_verify[slumber] - stub_uploader.metadata.InvalidRequires: Expected dependency requests to be listed in slumber's requires_dist
test_recursive_verify[python-xlib] - stub_uploader.metadata.InvalidRequires: Expected dependency Pillow to be listed in python-xlib's requires_dist
test_recursive_verify[JACK-Client] - stub_uploader.metadata.InvalidRequires: Expected dependency cffi to be listed in JACK-Client's requires_dist
test_recursive_verify[setuptools] - stub_uploader.metadata.InvalidRequires: Expected dependency docutils to be listed in setuptools's requires_dist
test_recursive_verify[PyAutoGUI] - stub_uploader.metadata.InvalidRequires: Expected dependency PyScreeze to be listed in PyAutoGUI's requires_dist
test_recursive_verify[tzlocal] - stub_uploader.metadata.InvalidRequires: Expected dependency pytz to be listed in tzlocal's requires_dist
test_recursive_verify[D3DShot] - stub_uploader.metadata.InvalidRequires: Expected dependency Pillow to be listed in D3DShot's requires_dist
So it may be that there's some reasonable improvement that could be made to the check that works for these twelve packages. Or maybe we can just require these specific stubs to have individualised exceptions committed to stub_uploader. Or maybe it's not super viable to plug this hole and we need to keep the allowlist forever.
(Also note that the implementation of the check in that diff^ isn't perfect, since it only works for projects that are on PyPI and have wheels, and only checks the dependencies of the latest version)