diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1728374..1e70b51a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
### Chore & Maintenance
- Update `jackson-databind`, `jackson-core`, and `jackson-annotations` dependencies to the 2.21 LTS line due to vulnerability
+- Update `jackson-databind` and `jackson-core` to 2.21.7 to fix CVE-2026-91776
- Update `lf-api-client-core` dependency to 2.2.5
## 1.0.3
diff --git a/pom.xml b/pom.xml
index d9b472b8..c523a127 100644
--- a/pom.xml
+++ b/pom.xml
@@ -309,7 +309,7 @@
${java.version}
1.8.0
2.0.0
- 2.21.6
+ 2.21.7
2.21
2.9.0
1.3.5
diff --git a/src/test/java/com/laserfiche/repository/api/unit/EntryDeserializationTest.java b/src/test/java/com/laserfiche/repository/api/unit/EntryDeserializationTest.java
new file mode 100644
index 00000000..03260ae5
--- /dev/null
+++ b/src/test/java/com/laserfiche/repository/api/unit/EntryDeserializationTest.java
@@ -0,0 +1,100 @@
+// Copyright (c) Laserfiche.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+package com.laserfiche.repository.api.unit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer;
+import com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase;
+import com.laserfiche.api.client.deserialization.TokenClientObjectMapper;
+import com.laserfiche.repository.api.clients.impl.model.Document;
+import com.laserfiche.repository.api.clients.impl.model.Entry;
+import com.laserfiche.repository.api.clients.impl.model.EntryType;
+import com.laserfiche.repository.api.clients.impl.model.Folder;
+import com.laserfiche.repository.api.clients.impl.model.RecordSeries;
+import com.laserfiche.repository.api.clients.impl.model.Shortcut;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+public class EntryDeserializationTest {
+ @Test
+ void readEntry_RepeatedUnknownTypeUsesFallback() throws ReflectiveOperationException {
+ TokenClientObjectMapper mapper = new TokenClientObjectMapper();
+ for (int i = 0; i < 10000; i++) {
+ assertFallback(mapper, "unknown");
+ }
+ assertEquals(1, typeIdCache(mapper).size());
+ }
+
+ @Test
+ void readEntry_DistinctUnknownTypesHaveBoundedCache() throws ReflectiveOperationException {
+ TokenClientObjectMapper mapper = new TokenClientObjectMapper();
+ for (int i = 0; i < 10000; i++) {
+ assertFallback(mapper, "unknown-" + i);
+ }
+ Map, ?> cache = typeIdCache(mapper);
+ assertTrue(cache.size() > 0);
+ assertTrue(cache.size() <= 1000, "Unknown type IDs must not grow the cache beyond Jackson's bound");
+ Entry folder = mapper.readValue("{\"entryType\":\"Folder\",\"id\":42}", Entry.class);
+ assertEquals(Folder.class, folder.getClass());
+ assertEquals(EntryType.FOLDER, folder.getEntryType());
+ }
+
+ @Test
+ void readEntry_OverlongUnknownTypesAreNotCached() throws ReflectiveOperationException {
+ TokenClientObjectMapper mapper = new TokenClientObjectMapper();
+ char[] characters = new char[257];
+ Arrays.fill(characters, 'x');
+ String prefix = new String(characters);
+ for (int i = 0; i < 100; i++) {
+ assertFallback(mapper, prefix + i);
+ }
+ assertEquals(0, typeIdCache(mapper).size());
+ }
+
+ @Test
+ void readEntry_KnownTypesStillSelectTheirSubtypes() {
+ TokenClientObjectMapper mapper = new TokenClientObjectMapper();
+ assertKnownType(mapper, EntryType.DOCUMENT, Document.class);
+ assertKnownType(mapper, EntryType.FOLDER, Folder.class);
+ assertKnownType(mapper, EntryType.SHORTCUT, Shortcut.class);
+ assertKnownType(mapper, EntryType.RECORD_SERIES, RecordSeries.class);
+ }
+
+ private static void assertFallback(TokenClientObjectMapper mapper, String typeId) {
+ Entry entry = mapper.readValue("{\"entryType\":\"" + typeId + "\",\"id\":42}", Entry.class);
+ assertEquals(Entry.class, entry.getClass());
+ assertNull(entry.getEntryType());
+ assertEquals(Integer.valueOf(42), entry.getId());
+ }
+
+ private static void assertKnownType(
+ TokenClientObjectMapper mapper, EntryType entryType, Class extends Entry> subtype) {
+ Entry entry = mapper.readValue("{\"entryType\":\"" + entryType.getValue() + "\",\"id\":42}", Entry.class);
+ assertEquals(subtype, entry.getClass());
+ assertEquals(entryType, entry.getEntryType());
+ assertEquals(Integer.valueOf(42), entry.getId());
+ }
+
+ private static Map, ?> typeIdCache(TokenClientObjectMapper mapper) throws ReflectiveOperationException {
+ // Observe retained keys on the actual mapper; successful fallback reads alone cannot detect this regression.
+ Field mapperField = TokenClientObjectMapper.class.getDeclaredField("jacksonMapper");
+ mapperField.setAccessible(true);
+ ObjectMapper jackson = ObjectMapper.class.cast(mapperField.get(mapper));
+ Field rootsField = ObjectMapper.class.getDeclaredField("_rootDeserializers");
+ rootsField.setAccessible(true);
+ Map, ?> roots = Map.class.cast(rootsField.get(jackson));
+ TypeWrappedDeserializer root =
+ TypeWrappedDeserializer.class.cast(roots.get(jackson.constructType(Entry.class)));
+ Field typeField = TypeWrappedDeserializer.class.getDeclaredField("_typeDeserializer");
+ typeField.setAccessible(true);
+ Field cacheField = TypeDeserializerBase.class.getDeclaredField("_deserializers");
+ cacheField.setAccessible(true);
+ return Map.class.cast(cacheField.get(typeField.get(root)));
+ }
+}