feat(kmz): add archive limits and use temp_directory - #24
Merged
Conversation
extractAllFiles() read an archive with no ceiling on entry count or uncompressed size, so a KMZ declaring a handful of entries that expand into gigabytes filled the disk before anything noticed. Entry names went unchecked too. Both are now validated before a single byte is read, and either limit can be turned off by setting it to 0. The mkdir() return value was ignored, so a destination that could not be created produced a warning and then a confusing failure further down. It now throws with the path that could not be made. temp_directory has been in the config since the first release without anything reading it. extractAllFiles() takes an optional destination and falls back to that key, then to the system temp directory, giving each call a directory of its own. extractAllFiles() also stops throwing bare KmlException for a missing or unreadable archive and uses KmzExtractorException like the rest of the class. That is a narrowing, KmzExtractorException extends KmlException.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
1. No ceiling on what an archive may expand into
A KMZ is a ZIP. A ZIP can declare a handful of entries that expand into far more than the machine has.
extractAllFiles()extracted whatever it was given, with no check on entry count or uncompressed size, andextractKmlContent()read an entry straight into a PHP string with no check on its size either. Point either at a hostile archive and the disk fills or the worker runs out of memory.Both now validate the archive before reading anything out of it:
max_archive_entries, default 5000max_uncompressed_size, default 256 MB, summed across entries fromstatIndex()Deliberately generous. A real KMZ is a KML plus its icons, nowhere near either number. Set either to
0to turn it off.2. Entry names were not checked
Nothing looked at where an entry claimed to live.
ZipArchive::extractTo()does sanitise paths, so this was not exploitable in practice, but the package was relying on that rather than deciding it. Entries whose name is absolute, starts with a drive letter, or contains a..segment are now rejected outright with the offending name in the message. Tested against../escaped.txt,images/../../escaped.txtand/etc/passwd.3.
mkdir()return value ignoredA destination that could not be created raised a warning and then failed further down with something unrelated to the actual cause. It now throws
Unable to create the extraction directory: <path>.The call is
@mkdir(...)because the return value is what gets acted on, and an application converting warnings to exceptions would otherwise surface PHP'smkdir(): Not a directoryinstead of the exception carrying the path. The secondis_dir()after it covers another process winning the race.4.
temp_directorydid nothingIt has been in the config since the first release, with a comment promising it "determines the temporary directory used for extracting KMZ files", and nothing has ever read it.
extractAllFiles()now takes an optional destination:Each call gets its own
kml-parser-<uniqid>directory underneath, so two concurrent extractions do not write over each other.defaultDestination()is public so a caller can find out where the files went.Also
extractAllFiles()threw bareKmlExceptionfor a missing file and an unopenable archive, whileextractKmlContent()threwKmzExtractorExceptionfor the same two conditions. Now consistent. This is a narrowing rather than a break:KmzExtractorException extends KmlException, so an existingcatch (KmlException)is unaffected.extractKmlContent()also stopped callingrange(0, $zip->numFiles - 1), which builds an array of every index in the archive just to find the first.kml.Tests
tests/KmzHardeningTest.php, 10 tests building real archives on disk:0means no limit, for bothtemp_directorytemp_directory,defaultDestination()falls back to the system temp directorySuite 69 to 79. PHPStan and Pint clean. README updated for the new config keys and the optional destination.
Note
The defaults are a judgement call. 5000 entries and 256 MB are far above any real KMZ I can find but are not derived from a spec. If a legitimate archive trips them, the fix is to raise the config value, and the exception message says which limit was hit.