This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git


The following commit(s) were added to refs/heads/master by this push:
     new c3b0da06f Reject tar sparse blocks larger than the entry size (#780)
c3b0da06f is described below

commit c3b0da06f7073bf79803e4ed818fb1a7319dca11
Author: KALI 834X <[email protected]>
AuthorDate: Thu Jul 9 17:23:24 2026 +0530

    Reject tar sparse blocks larger than the entry size (#780)
    
    * reject tar sparse blocks larger than the entry size
    
    * Replace magic numbers in sparse test helpers with constants
    
    * Potential fix for pull request finding
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    ---------
    
    Co-authored-by: Gary Gregory <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../archivers/tar/TarArchiveInputStream.java       |  9 +++
 .../commons/compress/archivers/tar/TarFile.java    |  8 ++
 .../compress/archivers/tar/SparseFilesTest.java    | 93 ++++++++++++++++++++++
 3 files changed, 110 insertions(+)

diff --git 
a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java
 
b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java
index dbbb31d7e..e7a3b7128 100644
--- 
a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java
+++ 
b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java
@@ -363,6 +363,8 @@ private void buildSparseInputStreams() throws IOException {
         final InputStream zeroInputStream = new 
TarArchiveSparseZeroInputStream(); // NOSONAR
         // logical offset into the extracted entry
         long offset = 0;
+        // physical bytes read from the archive for this entry
+        long dataBytes = 0;
         for (final TarArchiveStructSparse sparseHeader : sparseHeaders) {
             final long zeroBlockSize = sparseHeader.getOffset() - offset;
             if (zeroBlockSize < 0) {
@@ -380,6 +382,13 @@ private void buildSparseInputStreams() throws IOException {
             }
             // only store the input streams with non-zero size
             if (sparseHeader.getNumbytes() > 0) {
+                dataBytes += sparseHeader.getNumbytes();
+                // the non-hole blocks are read from the underlying stream, so 
their total size must not
+                // exceed the entry's stored size, otherwise reading this 
entry would consume bytes that
+                // belong to following entries
+                if (dataBytes > currEntry.getSize()) {
+                    throw new ArchiveException("Corrupted TAR archive. Sparse 
blocks for '%s' are larger than the entry size.", currEntry.getName());
+                }
                 // @formatter:off
                 sparseInputStreams.add(BoundedInputStream.builder()
                         .setInputStream(in)
diff --git 
a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java 
b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
index 2dd1b443a..7e250e497 100644
--- a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
@@ -358,6 +358,8 @@ private void buildSparseInputStreams() throws IOException {
         // logical offset into the extracted entry
         long offset = 0;
         long numberOfZeroBytesInSparseEntry = 0;
+        // physical bytes backing this entry
+        long dataBytes = 0;
         for (final TarArchiveStructSparse sparseHeader : sparseHeaders) {
             final long zeroBlockSize = sparseHeader.getOffset() - offset;
             if (zeroBlockSize < 0) {
@@ -371,6 +373,12 @@ private void buildSparseInputStreams() throws IOException {
             }
             // only store the input streams with non-zero size
             if (sparseHeader.getNumbytes() > 0) {
+                dataBytes = ArchiveException.addExact(dataBytes, 
sparseHeader.getNumbytes());
+                // the non-hole blocks map onto real bytes of the archive, so 
their total size must not
+                // exceed the entry's stored size, otherwise this entry would 
reach into following entries
+                if (dataBytes > currEntry.getSize()) {
+                    throw new ArchiveException("Corrupted TAR archive. Sparse 
blocks for '%s' are larger than the entry size.", currEntry.getName());
+                }
                 final long start = currEntry.getDataOffset() + 
sparseHeader.getOffset() - numberOfZeroBytesInSparseEntry;
                 if (start + sparseHeader.getNumbytes() < start) {
                     // possible integer overflow
diff --git 
a/src/test/java/org/apache/commons/compress/archivers/tar/SparseFilesTest.java 
b/src/test/java/org/apache/commons/compress/archivers/tar/SparseFilesTest.java
index ce39e07a8..d503802ba 100644
--- 
a/src/test/java/org/apache/commons/compress/archivers/tar/SparseFilesTest.java
+++ 
b/src/test/java/org/apache/commons/compress/archivers/tar/SparseFilesTest.java
@@ -24,10 +24,13 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.jupiter.api.Assumptions.assumeFalse;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -36,6 +39,7 @@
 import java.util.List;
 
 import org.apache.commons.compress.AbstractTest;
+import org.apache.commons.compress.archivers.ArchiveException;
 import org.apache.commons.compress.archivers.TestArchiveGenerator;
 import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.BeforeAll;
@@ -49,6 +53,9 @@
 
 class SparseFilesTest extends AbstractTest {
 
+    // numbytes claimed by the crafted sparse map, deliberately larger than 
the entry's stored size
+    private static final long SPARSE_MAP_NUMBYTES = 1024 * 1024;
+
     @TempDir
     private static Path tempDir;
 
@@ -532,4 +539,90 @@ void testTarFilePaxGNU() throws IOException {
             assertPaxGNUEntry(entries.get(2), "1.0");
         }
     }
+
+    private static void appendPadded(final ByteArrayOutputStream out, final 
byte[] data) {
+        out.write(data, 0, data.length);
+        final int rem = data.length % TarConstants.DEFAULT_RCDSIZE;
+        if (rem != 0) {
+            out.write(new byte[TarConstants.DEFAULT_RCDSIZE - rem], 0, 
TarConstants.DEFAULT_RCDSIZE - rem);
+        }
+    }
+
+    private static String paxRecord(final String key, final String value) {
+        final int size = key.length() + value.length() + 3; // ' ', '=', '\n'
+        int len = size + Integer.toString(size).length();
+        while (len != size + Integer.toString(len).length()) {
+            len = size + Integer.toString(len).length();
+        }
+        return len + " " + key + "=" + value + "\n";
+    }
+
+    private static void putField(final byte[] header, final int offset, final 
int length, final String value) {
+        final byte[] bytes = 
value.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+        System.arraycopy(bytes, 0, header, offset, Math.min(length, 
bytes.length));
+    }
+
+    private static void putOctal(final byte[] header, final int offset, final 
int length, final long value) {
+        putField(header, offset, length, String.format("%0" + (length - 1) + 
"o", value));
+    }
+
+    private static byte[] ustarHeader(final String name, final long size, 
final char typeFlag) {
+        final byte[] header = new byte[TarConstants.DEFAULT_RCDSIZE];
+        putField(header, 0, 100, name);
+        putOctal(header, 100, 8, 0644);
+        putOctal(header, 108, 8, 0);
+        putOctal(header, 116, 8, 0);
+        putOctal(header, 124, 12, size);
+        putOctal(header, 136, 12, 0);
+        java.util.Arrays.fill(header, 148, 156, (byte) ' '); // checksum 
placeholder
+        header[156] = (byte) typeFlag;
+        putField(header, 257, 6, "ustar");
+        header[263] = '0';
+        header[264] = '0';
+        long checksum = 0;
+        for (final byte b : header) {
+            checksum += b & 0xff;
+        }
+        putField(header, 148, 6, String.format("%06o", checksum));
+        header[154] = 0;
+        header[155] = ' ';
+        return header;
+    }
+
+    // Builds a PAX GNU 0.1 sparse entry whose stored size is 4 bytes but 
whose sparse map claims a
+    // single 1 MiB data block. realsize equals the block size, so the 
existing realSize check passes.
+    // A second entry follows, whose bytes must not be reachable through the 
sparse entry.
+    private byte[] sparseArchiveWithNumbytesLargerThanSize() {
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        final String pax = paxRecord("GNU.sparse.size", 
String.valueOf(SPARSE_MAP_NUMBYTES)) + paxRecord("GNU.sparse.map", "0," + 
SPARSE_MAP_NUMBYTES);
+        final byte[] paxData = 
pax.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+        appendPadded(bos, ustarHeader("PaxHeaders/sparse.bin", paxData.length, 
'x'));
+        appendPadded(bos, paxData);
+        appendPadded(bos, ustarHeader("sparse.bin", 4, '0'));
+        appendPadded(bos, new byte[] { 1, 2, 3, 4 });
+        final byte[] payload = "the following 
entry".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+        appendPadded(bos, ustarHeader("next.bin", payload.length, '0'));
+        appendPadded(bos, payload);
+        final int eofLength = 2 * TarConstants.DEFAULT_RCDSIZE; // two zero 
EOF records
+        bos.write(new byte[eofLength], 0, eofLength);
+        return bos.toByteArray();
+    }
+
+    @Test
+    void testRejectsSparseBlocksLargerThanEntrySizeTarArchiveInputStream() 
throws IOException {
+        final byte[] archive = sparseArchiveWithNumbytesLargerThanSize();
+        try (TarArchiveInputStream tis = new TarArchiveInputStream(new 
ByteArrayInputStream(archive))) {
+            assertThrows(ArchiveException.class, tis::getNextEntry);
+        }
+    }
+
+    @Test
+    void testRejectsSparseBlocksLargerThanEntrySizeTarFile() throws 
IOException {
+        final byte[] archive = sparseArchiveWithNumbytesLargerThanSize();
+        assertThrows(ArchiveException.class, () -> {
+            try (TarFile tarFile = 
TarFile.builder().setByteArray(archive).get()) {
+                tarFile.getEntries();
+            }
+        });
+    }
 }

Reply via email to