fix(utils): reject path separators in wheel/sdist filenames - #1337
fix(utils): reject path separators in wheel/sdist filenames#1337r266-tech wants to merge 6 commits into
Conversation
| # The last path component is percent-encoded, so decode it to the file name | ||
| return unquote(url_path.rsplit("/", 1)[-1]) | ||
| filename = unquote(url_path.rsplit("/", 1)[-1]) | ||
| if "/" in filename or "\\" in filename or "\0" in filename: |
There was a problem hiding this comment.
And/or, reject os.sep, os.altsep ? That would make the behaviour platform dependent, though, so maybe not.
There was a problem hiding this comment.
I'm thinking it should not be platform dependent? Not following closely though.
|
Please dismiss my approval. I think the |
|
Moved the separator/NUL check to parsing validation in
I cannot dismiss another user's approval as an external contributor, but the requested behavioral change is now on the branch. |
|
Actually, we already call So I suggest closing this as it will be addressed by #873. |
|
|
sbidoul
left a comment
There was a problem hiding this comment.
We could add a pylock-specific test, but the validation must be done in parse_wheel_filename (should be ok) and parse_sdist_filename.
b55ec2d to
3636593
Compare
|
Reworked in
Focused verification: 148 tests passed, Ruff check/format passed, and |
| """ | ||
| if "/" in filename or "\\" in filename or "\0" in filename: | ||
| raise InvalidWheelFilename(f"Invalid wheel filename: {filename!r}") | ||
|
|
There was a problem hiding this comment.
This change is unnecessary. Something in this function already catches this.
There was a problem hiding this comment.
I kept the whole-filename guard after finding that the existing parser accepts these characters outside the project-name field—for example, example-1.0-1/../evil-py3-none-any.whl parses with /../evil as the build suffix, and separators are also accepted in tag components. a50fa5e moves the regression cases into those build/tag positions so the guard's necessity is covered.
There was a problem hiding this comment.
Interesting find. Still I would rather address that by refining parse_tag() and _build_tag_regex. I've not checked, but surely the specification for these things do not allow arbitrary characters?
There was a problem hiding this comment.
Implemented that parser-boundary approach in fcfaa6f.
parse_tag()now rejects non-ASCII or otherwise invalid interpreter/ABI/platform components;_build_tag_regexnow uses an ASCII-digit prefix plusfullmatch(), rejecting/,\\, and NUL while preserving the existing free-form suffix behavior (covered by a positive1+vendorcase);parse_sdist_filename()continues to validate the name throughcanonicalize_name(..., validate=True)rather than a separate character list.
I removed the whole-wheel-filename guard, rebased the unchanged patch onto current main, and reran the focused parser/Pylock suite: 384 tests passed, with Ruff and diff checks clean. The full suite on the same patch also passed previously (62,360 passed, 1 platform skip).
| .. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name | ||
| """ | ||
| if "/" in filename or "\\" in filename or "\0" in filename: | ||
| raise InvalidSdistFilename(f"Invalid sdist filename: {filename!r}") |
There was a problem hiding this comment.
If hard coding these characters is unnecessary for wheel filenames, then there is probably a better way to do it for sdist filenames.
There was a problem hiding this comment.
Replaced the sdist character list with canonicalize_name(name_part, validate=True), translating InvalidName to InvalidSdistFilename. a50fa5e adds general invalid-name/cause coverage plus path and %00 Pylock cases.
a50fa5e to
fcfaa6f
Compare
| try: | ||
| name = canonicalize_name(name_part, validate=True) | ||
| except InvalidName as e: | ||
| raise InvalidSdistFilename(f"Invalid project name: {filename!r}") from e |
There was a problem hiding this comment.
| raise InvalidSdistFilename(f"Invalid project name: {filename!r}") from e | |
| raise InvalidSdistFilename(f"Invalid sdist filename (invalid project name): {filename!r}") from e |
| """ | ||
|
|
||
|
|
||
| _tag_component_regex = re.compile(r"\w+", re.ASCII) |
There was a problem hiding this comment.
PEP 425 says The platform tag is simply distutils.util.get_platform() with all hyphens - and periods . replaced with underscore _. In its FAQ it has Why normalise hyphens and other non-alphanumeric characters to underscores?.
So I suppose this regex is ok?
There was a problem hiding this comment.
I think so based on the definition of \w from the re docs.
| raise InvalidTag(f"Tag {tag!r} has an empty component: {component!r}") | ||
| for part in parts: | ||
| if _tag_component_regex.fullmatch(part) is None: | ||
| raise InvalidTag(f"Tag {tag!r} has an invalid component: {part!r}") |
There was a problem hiding this comment.
| raise InvalidTag(f"Tag {tag!r} has an invalid component: {part!r}") | |
| component = ".".join(parts) | |
| raise InvalidTag(f"Tag {tag!r} has an invalid component: {component!r}") |
?
|
In addition to the PR title, the changelog should also mention that |
|
|
||
| name = canonicalize_name(name_part) | ||
| try: | ||
| name = canonicalize_name(name_part, validate=True) |
There was a problem hiding this comment.
While this sounds correct, parse_wheel_filename does not use canonicalize_name(validate=True) and accepts non ascii characters (foo_bár is tested), which are rejected by canonicalize_name(validate=True) and is_normalized_name(). However parse_wheel_filename is typed to return NormalizedName.
PEP 427 explicitly allows non ascii characters in the project name: The archive filename is Unicode. It will be some time before the tools are updated to support non-ASCII filenames, but they are supported in this specification.
So there is a discrepancy that's probably worth discussing separately.
|
Added the requested changelog entry; it now explicitly calls out that A post-change adversarial review also found one remaining cross-platform filename edge: |
Summary
/,\\, and NUL characters inparse_wheel_filename()andparse_sdist_filename()PackageWheel.filenameandPackageSdist.filenamenon-raising for manually constructed objectsWhy
#1314 correctly percent-decodes the final URL path component, including valid names such as
%2Blocal versions. The same decoding can also produce path-shaped values such asexample/../evil-1.0.tar.gz.The public filename parsers are the canonical validation boundary, and pylock already delegates wheel/sdist validation to them. Rejecting separators and NUL there protects pylock and other downstream callers consistently while preserving non-raising filename property access.
Testing
.venv/bin/python -m pytest tests/test_utils.py tests/test_pylock.py -q(148 passed)uvx ruff check src/packaging/utils.py src/packaging/pylock.py tests/test_utils.py tests/test_pylock.pyuvx ruff format --check src/packaging/utils.py src/packaging/pylock.py tests/test_utils.py tests/test_pylock.pygit diff --check