Repository navigation
zipfile.Path regression #123270
Copy link
Copy link
Closed
jaraco/zipp
#124Labels
3.10 (EOL)end of lifeend of life3.11only security fixesonly security fixes3.12only security fixesonly security fixes3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixes3.8 (EOL)end of lifeend of life3.9 (EOL)end of lifeend of lifestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or errortype-securityA security issueA security issue
Description
Bug report
Bug description:
#122906 introduced a regression with directories that look like Windows drive letters (on Linux):
>>> import io, zipfile
>>> zf = zipfile.ZipFile(io.BytesIO(), "w")
>>> zf.writestr("d:/foo", "bar")
>>> zf.extractall("a")
>>> open("a/d:/foo").read()
'bar'
>>> p = zipfile.Path(zf)
>>> x = p / "d" / "foo"
>>> y = p / "d:" / "foo"
>>> list(p.iterdir()) # before: [Path(None, 'd:/')]
[Path(None, 'd/')]
>>> p.root.namelist() # before: ['d:/foo', 'd:/']
['d/foo', 'd/']
>>> x.exists() # before: False
True
>>> y.exists() # before: True
False
>>> zf.extractall("b") # before: worked like above
KeyError: "There is no item named 'd/foo' in the archive"
>>> x.read_text() # before: FileNotFoundError
KeyError: "There is no item named 'd/foo' in the archive"
>>> y.read_text() # before: worked
FileNotFoundError: ...This is the result of _sanitize() unconditionally treating a directory that looks like a drive letter as such and removing the colon, regardless of operating system:
cpython/Lib/zipfile/_path/__init__.py
Line 141 in 58fdb16
| bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE) |
Whereas _extract_member() uses os.path.splitdrive() (which is a no-op on Linux):
cpython/Lib/zipfile/__init__.py
Line 1807 in 58fdb16
| arcname = os.path.splitdrive(arcname)[1] |
CPython versions tested on:
3.12
Operating systems tested on:
Linux
Linked PRs
- gh-123270: Replaced SanitizedNames with a more surgical fix. #123354
- [3.13] gh-123270: Replaced SanitizedNames with a more surgical fix. (GH-123354) #123410
- [3.12] gh-123270: Replaced SanitizedNames with a more surgical fix. (GH-123354) #123411
- [3.11] gh-123270: Replaced SanitizedNames with a more surgical fix. (GH-123354) #123425
- [3.10] gh-123270: Replaced SanitizedNames with a more surgical fix. (GH-123354) #123426
- [3.9] gh-123270: Replaced SanitizedNames with a more surgical fix. (GH-123354) #123432
- [3.8] gh-123270: Replaced SanitizedNames with a more surgical fix. (GH-123354) #123433
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
3.10 (EOL)end of lifeend of life3.11only security fixesonly security fixes3.12only security fixesonly security fixes3.13only security fixesonly security fixes3.14bugs and security fixesbugs and security fixes3.8 (EOL)end of lifeend of life3.9 (EOL)end of lifeend of lifestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or errortype-securityA security issueA security issue
Projects
- StatusShow more project fieldsDone
Activity
[-]zipfile.Path regression with directories that look like Windows drive letters (on Linux)[/-][+]zipfile.Path regression[/+]obfusk commentedon Aug 23, 2024
Paths like
foo//barandfoo\\baralso cause issues:ZeroIntensity commentedon Aug 23, 2024
cc @jaraco
obfusk commentedon Aug 23, 2024
Sanitising is good. But e.g.
.open()will not work if it can't map back to the original path to pass that toZipFile.open(). Though.joinpath()will of course never use//or\\, so opening these files already required constructing the path without using that. Not sure what's best here if opening files with paths affected by sanitising is meant to be supported.obfusk commentedon Aug 23, 2024
obfusk commentedon Aug 23, 2024
Keeping a
dictmapping sanitised paths back to the originals might be a good choice. Though.namelist()will still be "wrong" for the ZipFile (e.g. when calling.extractall()). And it doesn't handle "duplicates" (see above).jaraco commentedon Aug 24, 2024
My expectation was that paths affected by sanitizing would not be supported.
My goal is to provide a (perhaps constrained) interface that's portable and provides consistent behavior independent of platform.
I'll have to think about this some more
I was not expecting that
foo\barord:/foowere in use in zip files.Can you say more about the use-cases that are affected by these paths? What causes these zip files to exist? What does the author of such a zip file expect to happen when consumed by a zipfile client?
obfusk commentedon Aug 24, 2024
Are these file names common? Maybe not. But on Linux these are perfectly acceptable file names. And whilst they may indeed cause portability issues for Windows users and I would thus avoid them when creating ZIP files for use on Windows, the ZIP format also considers them perfectly acceptable.
I myself have quite a few music files with odd file names; song titles can be quite varied. And whilst I've sometimes run into problems extracting those files on other operating systems I have never had issues creating or listing ZIP files.
File names like
foo/..//./barcan be considered malformed and would not be generated by simply creating a ZIP file from an existing directory tree. Butd:/foo\\baris not malformed, just unusual (and perhaps not portable).Tools like
zipandunzipand my ownzipinfo.pyhandle them just fine. So I would expect the same from Python. And indeedzipfile.ZipFilehandles them just fine. Butzipfile.Pathnow rejects them.I generally expect to be able to take any directory tree and turn it into a ZIP file without having to worry about file names containing colons or backslashes. Or at the most that I might need to be careful when extracting them on Windows. I would not expect to have problems merely listing the contents, especially when not even using Windows.
So it is very surprising when
zipfile.Patheffectively rejects perfectly valid ZIP files just because they have "unsupported" file names, especially since it didn't do so before. I would perhaps expect this kind of backwards-incompatible change to happen in a major version, given a good reason, but not in a security patch. And for it to be clearly documented if the interface is constrained like this. It also fails in unexpected ways, instead of providing a clear error message that makes it clear the problem is an unsupported file name.obfusk commentedon Aug 24, 2024
And my expectation would be that such paths would be either explicitly rejected as malformed if they cannot reasonably be supported. Or when they can be, handled gracefully with a portable interface that maps sanitised names providing a consistent interface that can reasonably be supported onto the underlying file names instead of simply causing attempts to read files to fail because the underlying file name does not match the sanitised one. And before this change,
d:/foo\\barworked just fine.obfusk commentedon Aug 24, 2024
This worked perfectly fine before. Now it raises
FileNotFoundError.And if
d:/foo\\baris not considered a valid path byzipfile.Path, I'd expect an error. Instead it happily tells me there is a file namedd/foo/bar. Except when I try reading I get an error that says there is no such file:obfusk commentedon Aug 24, 2024
Note that it's not just
d:/foothat's no longer allowed, e.g.V: The New Mythology Suite.flacis now also a forbidden file name (thoughextractall()would remove the "V: " -- but only on Windows, and only when extracting, not listing files).Meanwhile, this seems incorrect:
obfusk commentedon Aug 24, 2024
Meanwhile, as I posted here, the only fix needed for the infinite loop would be the loop condition here:
cpython/Lib/zipfile/_path/__init__.py
Lines 53 to 55 in 52caaef
This should work:
Or this:
43 remaining items
[3.10] gh-123270: Replaced SanitizedNames with a more surgical fix. (G…
obfusk commentedon Sep 5, 2024
I think documenting the caveats would be good.
Something like that might be nice. Though perhaps it could be more generic instead of being part of
zipfile.Path, even ifzipfile.Pathwould then perhaps provide an extraction API using that?jaraco commentedon Sep 5, 2024
I opened two issues to track. I'm not planning on driving them, but I'm happy to guide and advise. Feel free to take one or both on, or help drive the conversation toward something that's acceptable. Thanks.
obfusk commentedon Sep 5, 2024
Thanks. I have too many other projects to work on currently to take this on, but happy to comment and review when I can :)
abj101 commentedon Sep 7, 2024
Hi,
I'm new to contributing in general and have some experience in programming. I wanted to start by helping with documentation. How can I get started on this?
ZeroIntensity commentedon Sep 7, 2024
I don't think there's anything here that needs documentation updates, since it's a bugfix. You should take a look at the devguide.
obfusk commentedon Sep 7, 2024
But #123726 is about updating the documentation to explain the caveats regarding name sanitation as a result of the discussion here :)
chore(deps): Bump zipp from 3.20.0 to 3.20.2 (#1996)
Bump zipp from 3.20.0 to 3.20.2 (#566)
Bump the dependabot group with 4 updates (#312)
chore(deps): Bump zipp from 3.20.0 to 3.21.0 in /experimenter/tests (#…
Bump the dependabot group across 1 directory with 7 updates (#65)
Bump the pip group with 4 updates (#220)
Bump the dependabot group across 1 directory with 7 updates (#65)