fix: only extract zip and tar family archives

`identifyFormat` accepted any extractor mholt identified, which put the 7z and rar decoders (`bodgit/sevenzip`, `rardecode`) on untrusted input with no test coverage. Only zip, tar and compressed tar are opened now, until other formats are deliberately supported.

This also fixes java resource adapters: a `.rar` that is really a zip used to be identified by name as RAR and fail to extract. An unsupported identification now falls through to the zip end-of-central-directory check, so it's read as the zip it is.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-24 11:29:34 -04:00
parent c9b4f9c085
commit 73397f1e8b
2 changed files with 28 additions and 5 deletions
+23 -5
View File
@@ -85,17 +85,17 @@ func archiveFileName(archivePath string) string {
//
// Identification from the head of the stream wins: a tar holding one jar ends in that jar's central
// directory, and reading the tar as a zip would lose the rest. The end of the stream is consulted
// only when the head identifies nothing, which is how a zip appended to a launcher script (a Spring
// Boot executable jar) is found.
// only when the head identifies nothing supported, which is how a zip appended to a launcher script (a
// Spring Boot executable jar) is found, and how a java resource adapter (a zip named .rar) is read.
func identifyFormat(ctx context.Context, name string, content ReaderAtSeeker) archives.Extractor {
if _, err := content.Seek(0, io.SeekStart); err != nil {
return nil
}
format, _, err := intFile.IdentifyArchive(ctx, name, content)
if err == nil {
// a bare compression format, such as a gzipped file that is not a tar, is not extractable
extractor, _ := format.(archives.Extractor)
return extractor
if extractor := supportedExtractor(format); extractor != nil {
return extractor
}
}
if HasZipEndOfCentralDirectory(content) {
return archives.Zip{}
@@ -103,6 +103,24 @@ func identifyFormat(ctx context.Context, name string, content ReaderAtSeeker) ar
return nil
}
// supportedExtractor admits only the zip and tar families, the tar family optionally compressed. Other
// formats mholt identifies, such as 7z and rar, pull in decoders that have not been exercised against
// untrusted input here, so they are not opened until they are deliberately supported. A bare
// compression format, such as a gzipped file that is not a tar, is not extractable either.
func supportedExtractor(format archives.Format) archives.Extractor {
switch f := format.(type) {
case archives.Zip:
return f
case archives.Tar:
return f
case archives.CompressedArchive:
if _, ok := f.Extraction.(archives.Tar); ok {
return f
}
}
return nil
}
// extract adds every entry of the archive. Reaching the disk limit stops the walk and marks the
// resolver truncated rather than failing.
func (r *Resolver) extract(ctx context.Context, format archives.Extractor, content ReaderAtSeeker, exclusions Exclusions) error {
+5
View File
@@ -38,6 +38,11 @@ func Test_identifyFormat(t *testing.T) {
{"test.zip", zipBytes(t, sampleFiles), archives.Zip{}},
{"test.tar.gz", tarGzBytes(t, sampleFiles), archives.CompressedArchive{}},
{"plain.txt", []byte("just text"), nil},
// identified by mholt, but not opened until their decoders are deliberately supported
{"test.7z", []byte("7z\xbc\xaf\x27\x1c\x00\x04"), nil},
{"test.rar", []byte("Rar!\x1a\x07\x01\x00"), nil},
// a java resource adapter archive is a zip that shares rar's extension
{"adapter.rar", zipBytes(t, sampleFiles), archives.Zip{}},
// the name can be claimed by identification, so the probe alone must not admit prose
{"app.jar", []byte("plain text, no central directory anywhere in it"), nil},
{"bundle", []byte("plain text, no central directory anywhere in it"), nil},