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]