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

pjfanning pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/poi.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 4b345391c5 Handle oversized uint32 offsets in QuillContents 
descriptors (#1077)
4b345391c5 is described below

commit 4b345391c5b58ccf6dc381b22a0dceb8d5c5f994
Author: metsw24-max <[email protected]>
AuthorDate: Tue May 26 03:00:01 2026 +0530

    Handle oversized uint32 offsets in QuillContents descriptors (#1077)
---
 .../org/apache/poi/hpbf/model/QuillContents.java   |  19 +++-
 .../poi/hpbf/model/TestQuillContentsOverflow.java  | 110 +++++++++++++++++++++
 2 files changed, 127 insertions(+), 2 deletions(-)

diff --git 
a/poi-scratchpad/src/main/java/org/apache/poi/hpbf/model/QuillContents.java 
b/poi-scratchpad/src/main/java/org/apache/poi/hpbf/model/QuillContents.java
index 74d766fbdc..fdc1bd3010 100644
--- a/poi-scratchpad/src/main/java/org/apache/poi/hpbf/model/QuillContents.java
+++ b/poi-scratchpad/src/main/java/org/apache/poi/hpbf/model/QuillContents.java
@@ -29,6 +29,7 @@ import org.apache.poi.poifs.filesystem.DirectoryNode;
 import org.apache.poi.util.IOUtils;
 import org.apache.poi.util.LittleEndian;
 import org.apache.poi.util.LocaleUtil;
+import org.apache.poi.util.RecordFormatException;
 
 /**
  * Read Quill Contents (/Quill/QuillSub/CONTENTS) from an HPBF (Publisher 
.pub) document
@@ -64,8 +65,22 @@ public final class QuillContents extends HPBFPart {
                 int optB = LittleEndian.getUShort(data, offset+8);
                 int optC = LittleEndian.getUShort(data, offset+10);
                 String bitType = new String(data, offset+12, 4, 
LocaleUtil.CHARSET_1252);
-                int from = (int)LittleEndian.getUInt(data, offset+16);
-                int len = (int)LittleEndian.getUInt(data, offset+20);
+                // 'from' and 'len' are stored as uint32; a crafted .pub with 
a value
+                // > Integer.MAX_VALUE used to be silently narrowed via a 
plain (int)
+                // cast, letting a wrapped negative value reach 
IOUtils.safelyClone's
+                // generic "Invalid offset\length specified" check (which then 
lost
+                // the original uint32 in its message). Validate up-front so 
the
+                // failure carries the offending value, matching the 
PointerFactory
+                // fix in PR #1076 / EMF fix in PR #1060.
+                final long fromU = LittleEndian.getUInt(data, offset+16);
+                final long lenU  = LittleEndian.getUInt(data, offset+20);
+                if (fromU > Integer.MAX_VALUE) {
+                    throw new RecordFormatException(
+                            "QuillContents bit offset " + fromU + " exceeds 
Integer.MAX_VALUE");
+                }
+                IOUtils.safelyAllocateCheck(lenU, 
EscherPart.getMaxRecordLength());
+                int from = (int)fromU;
+                int len  = (int)lenU;
 
                 byte[] bitData = IOUtils.safelyClone(data, from, len, 
EscherPart.getMaxRecordLength());
 
diff --git 
a/poi-scratchpad/src/test/java/org/apache/poi/hpbf/model/TestQuillContentsOverflow.java
 
b/poi-scratchpad/src/test/java/org/apache/poi/hpbf/model/TestQuillContentsOverflow.java
new file mode 100644
index 0000000000..0b5c227824
--- /dev/null
+++ 
b/poi-scratchpad/src/test/java/org/apache/poi/hpbf/model/TestQuillContentsOverflow.java
@@ -0,0 +1,110 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.hpbf.model;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.poi.hpbf.HPBFDocument;
+import org.apache.poi.poifs.filesystem.DirectoryEntry;
+import org.apache.poi.poifs.filesystem.POIFSFileSystem;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LocaleUtil;
+import org.apache.poi.util.RecordFormatException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The QuillContents bit descriptors store {@code from} and {@code len} as
+ * 32-bit unsigned integers. A crafted .pub with a value &gt;
+ * {@code Integer.MAX_VALUE} used to be silently narrowed via a plain
+ * {@code (int)} cast, letting a wrapped value reach
+ * {@code IOUtils.safelyClone}'s generic
+ * {@code "Invalid offset\length specified"} check — the original uint32 was
+ * lost in the message. Validate the uint32 values up-front via
+ * {@code IOUtils.safelyAllocateCheck} (length) and an explicit
+ * {@code RecordFormatException} (offset) so the failure carries the actual
+ * offending value, matching the {@code PointerFactory} fix in PR #1076.
+ */
+public final class TestQuillContentsOverflow {
+
+    /**
+     * Builds an in-memory .pub whose Quill/QuillSub/CONTENTS stream contains
+     * a single populated bit descriptor at slot 0 with the supplied uint32
+     * {@code from}/{@code len} values.
+     */
+    private static byte[] buildPubWithBit(long fromU32, long lenU32) throws 
IOException {
+        // 512-byte CONTENTS stream: 8-byte "CHNKINK " header + 24 ignored 
bytes,
+        // then 20x 24-byte bit descriptors. Only slot 0 is populated; the
+        // 0x18/0x00 sentinel marks it as having data, the rest of the 
descriptors
+        // start with 0x00 and are skipped by QuillContents.
+        byte[] qc = new byte[512];
+        System.arraycopy("CHNKINK ".getBytes(LocaleUtil.CHARSET_1252), 0, qc, 
0, 8);
+        int off = 0x20;
+        qc[off]     = 0x18;
+        qc[off + 1] = 0x00;
+        System.arraycopy("TEXT".getBytes(LocaleUtil.CHARSET_1252), 0, qc, off 
+ 2,  4);
+        System.arraycopy("TEXT".getBytes(LocaleUtil.CHARSET_1252), 0, qc, off 
+ 12, 4);
+        LittleEndian.putUInt(qc, off + 16, fromU32);
+        LittleEndian.putUInt(qc, off + 20, lenU32);
+
+        try (POIFSFileSystem fs = new POIFSFileSystem();
+             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+            DirectoryEntry quill = fs.getRoot().createDirectory("Quill");
+            DirectoryEntry quillSub = quill.createDirectory("QuillSub");
+            quillSub.createDocument("CONTENTS", new ByteArrayInputStream(qc));
+            // HPBFDocument also requires a top-level Contents stream and an
+            // Escher/EscherStm pair to construct the surrounding parts.
+            fs.getRoot().createDocument("Contents",
+                    new ByteArrayInputStream(new byte[0]));
+            DirectoryEntry escher = fs.getRoot().createDirectory("Escher");
+            escher.createDocument("EscherStm", new ByteArrayInputStream(new 
byte[0]));
+            escher.createDocument("EscherDelayStm", new 
ByteArrayInputStream(new byte[0]));
+            fs.writeFilesystem(bos);
+            return bos.toByteArray();
+        }
+    }
+
+    @Test
+    void testRejectsOversizedLength() throws IOException {
+        byte[] pub = buildPubWithBit(0x80L, 0x80000001L); // len wraps to 
negative as int
+        assertThrows(RecordFormatException.class, () -> {
+            try (HPBFDocument d = new HPBFDocument(new 
ByteArrayInputStream(pub))) {
+                // QuillContents parsing happens in the constructor; reaching 
here is failure.
+                d.getQuillContents();
+            }
+        });
+    }
+
+    @Test
+    void testRejectsOversizedOffset() throws IOException {
+        byte[] pub = buildPubWithBit(0xFFFFFFFFL, 0x10L); // from wraps to -1 
as int
+        RecordFormatException ex = assertThrows(RecordFormatException.class, 
() -> {
+            try (HPBFDocument d = new HPBFDocument(new 
ByteArrayInputStream(pub))) {
+                d.getQuillContents();
+            }
+        });
+        // The pre-cast check preserves the original uint32 (4294967295) in 
the message,
+        // unlike the wrapped int (-1) the legacy safelyClone path would have 
reported.
+        assertTrue(ex.getMessage().contains("4294967295"),
+                "expected original uint32 in message, got: " + 
ex.getMessage());
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to