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();
+ }
+ });
+ }
}