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 a3957f0f67 fix spin count loop issue (#1084)
a3957f0f67 is described below

commit a3957f0f67f1a3806d520025ebcf483218c519b9
Author: PJ Fanning <[email protected]>
AuthorDate: Mon May 25 22:57:13 2026 +0100

    fix spin count loop issue (#1084)
---
 .../poifs/crypt/agile/AgileEncryptionVerifier.java | 18 +++++++++
 .../poi/poifs/crypt/agile/TestAgileDecryptor.java  | 43 ++++++++++++++++++++++
 2 files changed, 61 insertions(+)

diff --git 
a/poi/src/main/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java
 
b/poi/src/main/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java
index add680a728..1fc3efaee2 100644
--- 
a/poi/src/main/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java
+++ 
b/poi/src/main/java/org/apache/poi/poifs/crypt/agile/AgileEncryptionVerifier.java
@@ -27,9 +27,23 @@ import org.apache.poi.poifs.crypt.HashAlgorithm;
  */
 public class AgileEncryptionVerifier extends EncryptionVerifier {
 
+    /**
+     * Maximum allowed spinCount to prevent CPU-exhaustion DoS via crafted 
documents.
+     * Values beyond this are rejected during parsing.
+     */
+    private static final int MAX_SPIN_COUNT = 1_000_000;
+
     private int keyBits = -1;
     private int blockSize = -1;
 
+    /**
+     * Maximum allowed spinCount to prevent CPU-exhaustion DoS via crafted 
documents.
+     * Values beyond this are rejected during parsing.
+     */
+    public static int getMaxSpinCount() {
+        return MAX_SPIN_COUNT;
+    }
+
     @SuppressWarnings("unused")
     public AgileEncryptionVerifier(String descriptor) {
         this(AgileEncryptionInfoBuilder.parseDescriptor(descriptor));
@@ -72,6 +86,10 @@ public class AgileEncryptionVerifier extends 
EncryptionVerifier {
 
         Integer spinCount = keyData.getSpinCount();
         if (spinCount != null) {
+            if (spinCount < 0 || spinCount > MAX_SPIN_COUNT) {
+                throw new EncryptedDocumentException("SpinCount " + spinCount +
+                        " is out of the allowed range [0, " + MAX_SPIN_COUNT + 
"]");
+            }
             setSpinCount(spinCount);
         }
         setEncryptedVerifier(keyData.getEncryptedVerifierHashInput());
diff --git 
a/poi/src/test/java/org/apache/poi/poifs/crypt/agile/TestAgileDecryptor.java 
b/poi/src/test/java/org/apache/poi/poifs/crypt/agile/TestAgileDecryptor.java
index 2ee6ba30e1..de573b8138 100644
--- a/poi/src/test/java/org/apache/poi/poifs/crypt/agile/TestAgileDecryptor.java
+++ b/poi/src/test/java/org/apache/poi/poifs/crypt/agile/TestAgileDecryptor.java
@@ -19,16 +19,19 @@ package org.apache.poi.poifs.crypt.agile;
 
 import static org.apache.poi.poifs.crypt.Decryptor.DEFAULT_POIFS_ENTRY;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Stream;
 
 import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
+import org.apache.poi.EncryptedDocumentException;
 import org.apache.poi.poifs.crypt.Decryptor;
 import org.apache.poi.poifs.crypt.EncryptionInfo;
 import org.apache.poi.poifs.crypt.EncryptionMode;
@@ -37,6 +40,8 @@ import org.apache.poi.poifs.filesystem.DocumentInputStream;
 import org.apache.poi.poifs.filesystem.POIFSFileSystem;
 import org.apache.poi.util.HexDump;
 import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.LittleEndianByteArrayInputStream;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -99,4 +104,42 @@ class TestAgileDecryptor {
                 HexDump.dump(encDocument, 0, 0) + " full encrypted \n" +
                 HexDump.dump(encData, 0, 0));
     }
+
+    @Test
+    void testExcessiveSpinCountIsRejected() throws Exception {
+        // Create a normal encrypted document
+        EncryptionInfo infoEnc = new EncryptionInfo(EncryptionMode.agile);
+        Encryptor enc = infoEnc.getEncryptor();
+        enc.confirmPassword("test");
+
+        byte[] encData;
+        try (POIFSFileSystem fsEnc = new POIFSFileSystem()) {
+            try (OutputStream os = enc.getDataStream(fsEnc)) {
+                os.write(new byte[16]);
+            }
+            UnsynchronizedByteArrayOutputStream bos = 
UnsynchronizedByteArrayOutputStream.builder().get();
+            fsEnc.writeFilesystem(bos);
+            encData = bos.toByteArray();
+        }
+
+        // Read the EncryptionInfo stream and replace the spinCount with a 
value exceeding the maximum
+        byte[] encInfoBytes;
+        try (POIFSFileSystem fsMod = new POIFSFileSystem(new 
ByteArrayInputStream(encData));
+             DocumentInputStream dis = 
fsMod.getRoot().createDocumentInputStream(EncryptionInfo.ENCRYPTION_INFO_ENTRY))
 {
+            encInfoBytes = IOUtils.toByteArray(dis);
+        }
+
+        // The first 8 bytes are version/flags; the remainder is the XML 
descriptor
+        String xml = new String(encInfoBytes, 8, encInfoBytes.length - 8, 
StandardCharsets.UTF_8);
+        String modifiedXml = xml.replaceAll("spinCount=\"\\d+\"",
+                "spinCount=\"" + (AgileEncryptionVerifier.getMaxSpinCount() + 
1) + "\"");
+
+        byte[] xmlBytes = modifiedXml.getBytes(StandardCharsets.UTF_8);
+        byte[] modifiedEncInfoBytes = new byte[8 + xmlBytes.length];
+        System.arraycopy(encInfoBytes, 0, modifiedEncInfoBytes, 0, 8);
+        System.arraycopy(xmlBytes, 0, modifiedEncInfoBytes, 8, 
xmlBytes.length);
+
+        assertThrows(EncryptedDocumentException.class, () ->
+                new EncryptionInfo(new 
LittleEndianByteArrayInputStream(modifiedEncInfoBytes), null));
+    }
 }


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

Reply via email to