From 8c393ca38241b00dc851177734530633fcfc8130 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 08:46:16 +0900 Subject: [PATCH 1/4] test(security): replay Jackson LTS baseline on live develop --- .../JacksonSecurityBaselineTest.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java new file mode 100644 index 00000000..f702c1f2 --- /dev/null +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java @@ -0,0 +1,61 @@ +package com.xtrmetl.etl.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Guards the shared Maven dependency-management boundary against Jackson Databind versions that + * remain inside the currently known vulnerable 2.21.x range. + */ +class JacksonSecurityBaselineTest { + + private static final Path PROJECT_ROOT = projectRoot(); + + @Test + void jacksonSecurityBomPrecedesImportedSpringBootDependencyManagement() throws IOException { + String pom = Files.readString(PROJECT_ROOT.resolve("pom.xml"), StandardCharsets.UTF_8); + + assertTrue( + pom.contains("2.21.5"), + "Root dependency management must pin Jackson 2.21.5, the current patched 2.21 LTS baseline" + ); + + String jacksonBom = "jackson-bom"; + String springBootBom = "spring-boot-dependencies"; + int jacksonIndex = pom.indexOf(jacksonBom); + int springBootIndex = pom.indexOf(springBootBom); + + assertTrue(jacksonIndex >= 0, "Root dependencyManagement must import the Jackson BOM explicitly"); + assertTrue(springBootIndex >= 0, "Root dependencyManagement must continue importing Spring Boot dependencies"); + assertTrue( + jacksonIndex < springBootIndex, + "Without the Spring Boot parent POM, the explicit Jackson override BOM must precede spring-boot-dependencies" + ); + } + + /** Finds the repository root from root- or module-scoped Maven execution. */ + private static Path projectRoot() { + Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path lastPomParent = null; + while (current != null) { + if (Files.exists(current.resolve(".git"))) { + return current; + } + if (Files.exists(current.resolve("pom.xml"))) { + lastPomParent = current; + } + current = current.getParent(); + } + if (lastPomParent != null) { + return lastPomParent; + } + throw new IllegalStateException("Could not find project root"); + } +} From 7e86513a7f04adef85aad109fac251e020b0251e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 08:51:00 +0900 Subject: [PATCH 2/4] fix(security): align Jackson with patched 2.21 LTS BOM --- pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pom.xml b/pom.xml index 7c0ee7c2..c5c46ae4 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ 25 3.5.16 2025.0.3 + 2.21.5 42.7.12 3.3.16 2.0.13 @@ -33,6 +34,13 @@ + + com.fasterxml.jackson + jackson-bom + ${jackson-bom.version} + pom + import + org.springframework.boot spring-boot-dependencies From 4cef299cdb1e637a9577891c281bf3f139aab10f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 09:25:47 +0900 Subject: [PATCH 3/4] test(security): reject Jackson BOM decoy evidence --- .../JacksonSecurityBaselineTest.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java index f702c1f2..5d52367d 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java @@ -8,11 +8,12 @@ import java.nio.file.Path; import java.nio.file.Paths; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Guards the shared Maven dependency-management boundary against Jackson Databind versions that - * remain inside the currently known vulnerable 2.21.x range. + * Guards the shared Maven dependency-management boundary against Jackson Databind versions below + * the selected patched 2.21.5 security baseline. */ class JacksonSecurityBaselineTest { @@ -22,6 +23,31 @@ class JacksonSecurityBaselineTest { void jacksonSecurityBomPrecedesImportedSpringBootDependencyManagement() throws IOException { String pom = Files.readString(PROJECT_ROOT.resolve("pom.xml"), StandardCharsets.UTF_8); + assertJacksonBomContract(pom); + } + + @Test + void rejectsJacksonBomDecoysOutsideRootDependencyManagement() { + String pomWithDecoy = """ + + + 2.21.5 + + + + + + spring-boot-dependencies + + + + + """; + + assertThrows(AssertionError.class, () -> assertJacksonBomContract(pomWithDecoy)); + } + + private static void assertJacksonBomContract(String pom) { assertTrue( pom.contains("2.21.5"), "Root dependency management must pin Jackson 2.21.5, the current patched 2.21 LTS baseline" From a222c29c158546861ebe11c714749fea22f54303 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 10:09:33 +0900 Subject: [PATCH 4/4] test(security): validate Jackson BOM structurally --- .../JacksonSecurityBaselineTest.java | 92 +++++++++++++++++-- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java index 5d52367d..582c3c18 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/documentation/JacksonSecurityBaselineTest.java @@ -1,13 +1,22 @@ package com.xtrmetl.etl.documentation; import org.junit.jupiter.api.Test; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; import java.io.IOException; +import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -48,17 +57,39 @@ void rejectsJacksonBomDecoysOutsideRootDependencyManagement() { } private static void assertJacksonBomContract(String pom) { - assertTrue( - pom.contains("2.21.5"), - "Root dependency management must pin Jackson 2.21.5, the current patched 2.21 LTS baseline" + Element project = parseProject(pom); + Element properties = requireDirectChild(project, "properties"); + assertEquals( + "2.21.5", + requireDirectChild(properties, "jackson-bom.version").getTextContent().trim(), + "Root properties must pin Jackson 2.21.5, the current patched 2.21 LTS baseline" ); - String jacksonBom = "jackson-bom"; - String springBootBom = "spring-boot-dependencies"; - int jacksonIndex = pom.indexOf(jacksonBom); - int springBootIndex = pom.indexOf(springBootBom); + Element dependencyManagement = requireDirectChild(project, "dependencyManagement"); + Element dependencies = requireDirectChild(dependencyManagement, "dependencies"); + List managedDependencies = directChildren(dependencies, "dependency"); + + int jacksonIndex = -1; + int springBootIndex = -1; + int jacksonCount = 0; + for (int index = 0; index < managedDependencies.size(); index++) { + Element dependency = managedDependencies.get(index); + String artifactId = directChildText(dependency, "artifactId"); + if ("jackson-bom".equals(artifactId)) { + jacksonCount++; + jacksonIndex = index; + assertEquals("com.fasterxml.jackson", directChildText(dependency, "groupId")); + assertEquals("${jackson-bom.version}", directChildText(dependency, "version")); + assertEquals("pom", directChildText(dependency, "type")); + assertEquals("import", directChildText(dependency, "scope")); + } + if ("spring-boot-dependencies".equals(artifactId)) { + assertEquals("org.springframework.boot", directChildText(dependency, "groupId")); + springBootIndex = index; + } + } - assertTrue(jacksonIndex >= 0, "Root dependencyManagement must import the Jackson BOM explicitly"); + assertEquals(1, jacksonCount, "Root dependencyManagement must import exactly one Jackson BOM"); assertTrue(springBootIndex >= 0, "Root dependencyManagement must continue importing Spring Boot dependencies"); assertTrue( jacksonIndex < springBootIndex, @@ -66,6 +97,51 @@ private static void assertJacksonBomContract(String pom) { ); } + private static Element parseProject(String pom) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder() + .parse(new InputSource(new StringReader(pom))) + .getDocumentElement(); + } catch (Exception exception) { + throw new AssertionError("Root pom.xml must be parseable XML for structural dependency validation", exception); + } + } + + private static Element requireDirectChild(Element parent, String name) { + for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof Element element && name.equals(elementName(element))) { + return element; + } + } + throw new AssertionError("Missing direct <" + name + "> under <" + elementName(parent) + ">"); + } + + private static List directChildren(Element parent, String name) { + List children = new ArrayList<>(); + for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof Element element && name.equals(elementName(element))) { + children.add(element); + } + } + return children; + } + + private static String directChildText(Element parent, String name) { + return requireDirectChild(parent, name).getTextContent().trim(); + } + + private static String elementName(Node node) { + return node.getLocalName() == null ? node.getNodeName() : node.getLocalName(); + } + /** Finds the repository root from root- or module-scoped Maven execution. */ private static Path projectRoot() { Path current = Paths.get(System.getProperty("user.dir")).toAbsolutePath();