diff --git a/VALIDATION.md b/VALIDATION.md index 0c1ea5d..5fde6b9 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -22,6 +22,25 @@ binds image build/tag to package and push to deploy. The test lifecycle stops before those phases. The controller tests use standalone MockMvc and do not start the application, connect to a Docker daemon, or deploy services. -CI executes the four route regressions on Java 8, 17, and 21 and archives Maven +CI executes the four route regressions and two logging compatibility tests on Java 8, 17, and 21 and archives Maven evidence. Boot 2.1.4, public production sources, JMX, and deployment settings are unchanged. No claim is made that all other old dependencies are secure. + +## Log4j follow-up + +Both applications previously resolved log4j-core 2.11.2 at compile scope, in the +Apache CVE-2021-44228 affected range. This dependency finding does not prove an +exploitable input path in a deployment. The Boot parent `log4j2.version` override +aligns API, Core, JUL, Web and the SLF4J 1.x bridge to 2.26.1. SLF4J remains 1.7.25; +no SLF4J 2.x or Jakarta bridge migration is required. + +Official release, requirements and security references: +https://logging.apache.org/log4j/2.x/release-notes.html +https://logging.apache.org/log4j/2.x/manual/installation.html +https://logging.apache.org/security.html + +The logging tests initialize Boot 2.1's actual Log4j2LoggingSystem and capture +messages through Log4j, SLF4J, Spring JCL and JUL. They verify message lookup +syntax remains literal and the default JNDI lookup is disabled. No network +lookup, application server or container is started. Dependency checks enforce +aligned modules without a competing Logback backend or reverse SLF4J bridge. diff --git a/pom.xml b/pom.xml index e485628..a0466cf 100644 --- a/pom.xml +++ b/pom.xml @@ -109,6 +109,7 @@ 4.12 3.1.0 1.7.25 + 2.26.1 diff --git a/scripts/check-dependency-management.py b/scripts/check-dependency-management.py index 0e6b703..163807f 100644 --- a/scripts/check-dependency-management.py +++ b/scripts/check-dependency-management.py @@ -17,3 +17,13 @@ tree = (directory / "target/dependency-tree.txt").read_text() assert "commons-io:commons-io:" not in tree, tree print(str(directory) + ": manages Commons IO 2.22.0; no actual Commons IO dependency") + +# Both application modules must use one aligned Log4j backend and the SLF4J 1.x bridge. +for module in ("spring-boot-deployment-app", "spring-boot-deployment-docker"): + tree = (Path(module) / "target/dependency-tree.txt").read_text() + artifacts = ("log4j-api", "log4j-core", "log4j-jul", "log4j-web", "log4j-slf4j-impl") + for artifact in artifacts: + assert "org.apache.logging.log4j:" + artifact + ":jar:2.26.1:compile" in tree, tree + assert "log4j-to-slf4j:" not in tree, "Circular logging bridge" + assert "logback-classic:" not in tree, "Competing logging backend" + print(module + ": aligned Log4j 2.26.1 with SLF4J 1.x bridge") diff --git a/spring-boot-deployment-app/src/test/java/com/github/vindell/deployment/LoggingCompatibilityTest.java b/spring-boot-deployment-app/src/test/java/com/github/vindell/deployment/LoggingCompatibilityTest.java new file mode 100644 index 0000000..006a17e --- /dev/null +++ b/spring-boot-deployment-app/src/test/java/com/github/vindell/deployment/LoggingCompatibilityTest.java @@ -0,0 +1,55 @@ +package com.github.vindell.deployment; + +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.springframework.boot.logging.LoggingInitializationContext; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem; +import org.springframework.core.env.StandardEnvironment; +import static org.junit.Assert.*; + +public class LoggingCompatibilityTest { + @Test + public void bootLoggingAndLegacyFacadesReachLog4jCore() { + LoggingSystem system = LoggingSystem.get(getClass().getClassLoader()); + assertTrue(system instanceof Log4J2LoggingSystem); + system.beforeInitialize(); + system.initialize(new LoggingInitializationContext(new StandardEnvironment()), null, null); + LoggerContext context = (LoggerContext) LogManager.getContext(false); + final List messages = new ArrayList<>(); + AbstractAppender appender = new AbstractAppender("capture", null, null, false, Property.EMPTY_ARRAY) { + @Override public void append(LogEvent event) { + messages.add(org.apache.logging.log4j.core.layout.PatternLayout.createDefaultLayout() + .toSerializable(event).trim()); + } + }; + appender.start(); + context.getRootLogger().addAppender(appender); + try { + assertEquals("org.apache.logging.slf4j.Log4jLoggerFactory", + org.slf4j.LoggerFactory.getILoggerFactory().getClass().getName()); + LogManager.getLogger(getClass()).info("direct-log4j"); + org.slf4j.LoggerFactory.getLogger(getClass()).info("legacy-slf4j"); + org.apache.commons.logging.LogFactory.getLog(getClass()).info("spring-jcl"); + java.util.logging.Logger.getLogger(getClass().getName()).info("jul-bridge"); + assertTrue(messages.toString(), messages.contains("direct-log4j")); + assertTrue(messages.toString(), messages.contains("legacy-slf4j")); + assertTrue(messages.toString(), messages.contains("spring-jcl")); + assertTrue(messages.toString(), messages.contains("jul-bridge")); + assertFalse(org.apache.logging.log4j.core.net.JndiManager.isJndiLookupEnabled()); + String literal = "${java:version}"; + LogManager.getLogger(getClass()).info(literal); + assertTrue(messages.contains(literal)); + } finally { + context.getRootLogger().removeAppender(appender); + appender.stop(); + system.cleanUp(); + } + } +} diff --git a/spring-boot-deployment-docker/src/test/java/com/github/vindell/deployment/LoggingCompatibilityTest.java b/spring-boot-deployment-docker/src/test/java/com/github/vindell/deployment/LoggingCompatibilityTest.java new file mode 100644 index 0000000..006a17e --- /dev/null +++ b/spring-boot-deployment-docker/src/test/java/com/github/vindell/deployment/LoggingCompatibilityTest.java @@ -0,0 +1,55 @@ +package com.github.vindell.deployment; + +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.springframework.boot.logging.LoggingInitializationContext; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem; +import org.springframework.core.env.StandardEnvironment; +import static org.junit.Assert.*; + +public class LoggingCompatibilityTest { + @Test + public void bootLoggingAndLegacyFacadesReachLog4jCore() { + LoggingSystem system = LoggingSystem.get(getClass().getClassLoader()); + assertTrue(system instanceof Log4J2LoggingSystem); + system.beforeInitialize(); + system.initialize(new LoggingInitializationContext(new StandardEnvironment()), null, null); + LoggerContext context = (LoggerContext) LogManager.getContext(false); + final List messages = new ArrayList<>(); + AbstractAppender appender = new AbstractAppender("capture", null, null, false, Property.EMPTY_ARRAY) { + @Override public void append(LogEvent event) { + messages.add(org.apache.logging.log4j.core.layout.PatternLayout.createDefaultLayout() + .toSerializable(event).trim()); + } + }; + appender.start(); + context.getRootLogger().addAppender(appender); + try { + assertEquals("org.apache.logging.slf4j.Log4jLoggerFactory", + org.slf4j.LoggerFactory.getILoggerFactory().getClass().getName()); + LogManager.getLogger(getClass()).info("direct-log4j"); + org.slf4j.LoggerFactory.getLogger(getClass()).info("legacy-slf4j"); + org.apache.commons.logging.LogFactory.getLog(getClass()).info("spring-jcl"); + java.util.logging.Logger.getLogger(getClass().getName()).info("jul-bridge"); + assertTrue(messages.toString(), messages.contains("direct-log4j")); + assertTrue(messages.toString(), messages.contains("legacy-slf4j")); + assertTrue(messages.toString(), messages.contains("spring-jcl")); + assertTrue(messages.toString(), messages.contains("jul-bridge")); + assertFalse(org.apache.logging.log4j.core.net.JndiManager.isJndiLookupEnabled()); + String literal = "${java:version}"; + LogManager.getLogger(getClass()).info(literal); + assertTrue(messages.contains(literal)); + } finally { + context.getRootLogger().removeAppender(appender); + appender.stop(); + system.cleanUp(); + } + } +}