From 181d3edab4ff1aa49d90c02a589760a6b4133c86 Mon Sep 17 00:00:00 2001
From: loong10k <20489781+loong10k@users.noreply.github.com>
Date: Wed, 4 Nov 2026 07:52:26 +0800
Subject: [PATCH 1/2] fix: align Jackson 2 security patches and expose upstream
build blockers
---
.github/workflows/ci.yml | 36 +++++-------
.mvn/ci-settings.xml | 11 ++++
pom.xml | 18 ++++--
.../JacksonJwtCompatibilityTest.java | 56 +++++++++++++++++++
4 files changed, 96 insertions(+), 25 deletions(-)
create mode 100644 .mvn/ci-settings.xml
create mode 100644 src/test/java/org/springframework/security/boot/jwt/authentication/JacksonJwtCompatibilityTest.java
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a9225bd..40cfc82 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -45,33 +45,27 @@ jobs:
tar -xzf /tmp/apache-maven-4-bin.tar.gz -C "$HOME/apache-maven-4" --strip-components=1
echo "$HOME/apache-maven-4/bin" >> "$GITHUB_PATH"
- - name: Install Easy4J snapshot dependencies
- env:
- EASY4J_BRANCH: ${{ github.base_ref || github.ref_name }}
- EASY4J_DEPENDENCIES: "jwt-issuer spring-security-extension"
+ - name: Build pinned public Easy4J snapshot sources
run: |
set -euo pipefail
dependency_root="${RUNNER_TEMP}/easy4j-dependencies"
mkdir -p "${dependency_root}"
- for dependency in ${EASY4J_DEPENDENCIES}; do
- git clone --depth 1 --branch "${EASY4J_BRANCH}" \
- "https://github.com/easy-4-java/${dependency}.git" \
- "${dependency_root}/${dependency}"
- for attempt in 1 2 3; do
- if mvn -B --no-transfer-progress \
- -f "${dependency_root}/${dependency}/pom.xml" \
- -Dmaven.test.skip=true -Djacoco.skip=true -Dgpg.skip=true install; then
- break
- fi
- if [[ "${attempt}" -eq 3 ]]; then
- exit 1
- fi
- sleep "$((attempt * 10))"
- done
- done
+ # Fixed public revisions: changing a sibling main must not silently
+ # change the snapshot classes tested by this repository.
+ while read -r dependency revision; do
+ git init "${dependency_root}/${dependency}"
+ git -C "${dependency_root}/${dependency}" remote add origin "https://github.com/easy-4-java/${dependency}.git"
+ git -C "${dependency_root}/${dependency}" fetch --depth 1 origin "${revision}"
+ git -C "${dependency_root}/${dependency}" checkout --detach FETCH_HEAD
+ test "$(git -C "${dependency_root}/${dependency}" rev-parse HEAD)" = "${revision}"
+ mvn -B --no-transfer-progress -s .mvn/ci-settings.xml -f "${dependency_root}/${dependency}/pom.xml" -Dgpg.skip=true clean install
+ done <<'SOURCES'
+ jwt-issuer fe29ffd8471eff70d2117b73e4cb2459e5ec4a2f
+ spring-security-extension 385e4ae3b16f369c94a9068afcb27fca47494767
+ SOURCES
- name: Build and verify with JaCoCo coverage gate
- run: mvn -B --no-transfer-progress clean verify
+ run: mvn -B --no-transfer-progress -s .mvn/ci-settings.xml clean verify
- name: Upload JaCoCo coverage report
if: always()
diff --git a/.mvn/ci-settings.xml b/.mvn/ci-settings.xml
new file mode 100644
index 0000000..f03aef6
--- /dev/null
+++ b/.mvn/ci-settings.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ central-public
+ *
+ https://repo.maven.apache.org/maven2
+
+
+
diff --git a/pom.xml b/pom.xml
index 35250b7..527d941 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,7 @@
3.20.0
2.0.x.20260630-SNAPSHOT
- 2.17.2
+ 2.18.12
4.0.1
6.1.0
5.18.0
@@ -98,11 +98,13 @@
${spring-framework.version}
provided
-
+
- com.fasterxml.jackson.core
- jackson-databind
+ com.fasterxml.jackson
+ jackson-bom
${jackson.version}
+ pom
+ import
@@ -222,6 +224,14 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.4
+
+ true
+
+
diff --git a/src/test/java/org/springframework/security/boot/jwt/authentication/JacksonJwtCompatibilityTest.java b/src/test/java/org/springframework/security/boot/jwt/authentication/JacksonJwtCompatibilityTest.java
new file mode 100644
index 0000000..0477435
--- /dev/null
+++ b/src/test/java/org/springframework/security/boot/jwt/authentication/JacksonJwtCompatibilityTest.java
@@ -0,0 +1,56 @@
+package org.springframework.security.boot.jwt.authentication;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import java.util.Base64;
+import java.util.Map;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import static org.junit.jupiter.api.Assertions.*;
+
+class JacksonJwtCompatibilityTest {
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @Test
+ void signedJwtJsonPreservesUnicodeAndClaimTypes() throws Exception {
+ String header = encode(mapper.writeValueAsBytes(Map.of("alg", "HS256", "typ", "JWT")));
+ String payload = encode(mapper.writeValueAsBytes(Map.of("sub", "用户", "roles", new String[]{"reader"}, "exp", 2000000000L)));
+ String input = header + "." + payload;
+ Mac mac = Mac.getInstance("HmacSHA256");
+ mac.init(new SecretKeySpec(new byte[32], "HmacSHA256"));
+ String jwt = input + "." + encode(mac.doFinal(input.getBytes(java.nio.charset.StandardCharsets.US_ASCII)));
+ String[] parts = jwt.split("\\.");
+ assertEquals(3, parts.length);
+ assertArrayEquals(mac.doFinal(input.getBytes(java.nio.charset.StandardCharsets.US_ASCII)), Base64.getUrlDecoder().decode(parts[2]));
+ JsonNode claims = mapper.readTree(Base64.getUrlDecoder().decode(parts[1]));
+ assertEquals("用户", claims.get("sub").asText());
+ assertEquals("reader", claims.get("roles").get(0).asText());
+ assertEquals(2000000000L, claims.get("exp").longValue());
+ JwtAuthenticationToken token = new JwtAuthenticationToken(claims.get("sub").asText(), jwt);
+ assertFalse(token.isAuthenticated());
+ assertThrows(IllegalArgumentException.class, () -> token.setAuthenticated(true));
+ token.eraseCredentials();
+ assertNull(token.getCredentials());
+ }
+
+ @Test
+ void rejectsMalformedClaimsAndDoesNotEnablePolymorphicTyping() throws Exception {
+ assertThrows(JsonProcessingException.class, () -> mapper.readTree("{\"sub\": [}"));
+ Object value = mapper.readValue("{\"@class\":\"java.lang.Runtime\",\"sub\":\"alice\"}", Object.class);
+ assertInstanceOf(Map.class, value);
+ assertEquals("alice", ((Map, ?>) value).get("sub"));
+ }
+
+ @Test
+ void rejectsOverlyNestedUntrustedClaims() {
+ String json = "[".repeat(1001) + "0" + "]".repeat(1001);
+ assertThrows(com.fasterxml.jackson.core.exc.StreamConstraintsException.class,
+ () -> mapper.readTree(json));
+ }
+
+ private static String encode(byte[] bytes) {
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
+ }
+}
From 02ada334f5f6c9f8f8e1450a575bf40f2e843630 Mon Sep 17 00:00:00 2001
From: loong10k <20489781+loong10k@users.noreply.github.com>
Date: Wed, 4 Nov 2026 08:13:01 +0800
Subject: [PATCH 2/2] fix(ci): upload reports only from the current verify
invocation
---
.github/scripts/run_verify.py | 55 ++++++++++++++++++++++++++
.github/scripts/test_run_verify.py | 62 ++++++++++++++++++++++++++++++
.github/workflows/ci.yml | 15 ++++++--
3 files changed, 129 insertions(+), 3 deletions(-)
create mode 100644 .github/scripts/run_verify.py
create mode 100644 .github/scripts/test_run_verify.py
diff --git a/.github/scripts/run_verify.py b/.github/scripts/run_verify.py
new file mode 100644
index 0000000..7cfecb7
--- /dev/null
+++ b/.github/scripts/run_verify.py
@@ -0,0 +1,55 @@
+"""Run verify after removing checkout artifacts; expose only this invocation's reports."""
+import argparse
+import json
+import os
+from pathlib import Path
+import shutil
+import subprocess
+
+
+def clean_target(root):
+ target = root / "target"
+ if target.is_symlink():
+ target.unlink()
+ elif target.exists():
+ shutil.rmtree(target)
+
+
+def run_verify(root, command):
+ clean_target(root)
+ # A failed Maven invocation must retain its exit status even if it wrote reports.
+ try:
+ status = subprocess.run(command, cwd=root, check=False).returncode
+ except OSError as error:
+ print(error)
+ status = 127
+ reports = {
+ "surefire": any((root / "target/surefire-reports").glob("TEST-*.xml")),
+ "jacoco": (root / "target/site/jacoco/jacoco.xml").is_file(),
+ }
+ output = os.environ.get("GITHUB_OUTPUT")
+ if output:
+ with open(output, "a", encoding="utf-8") as stream:
+ for name, present in reports.items():
+ stream.write(f"{name}={str(present).lower()}\n")
+ for name, present in reports.items():
+ if present:
+ directory = root / ("target/surefire-reports" if name == "surefire" else "target/site/jacoco")
+ (directory / "ci-origin.json").write_text(json.dumps({
+ "sha": os.environ.get("GITHUB_SHA"),
+ "run_id": os.environ.get("GITHUB_RUN_ID"),
+ "run_attempt": os.environ.get("GITHUB_RUN_ATTEMPT"),
+ "verify_exit_code": status,
+ }) + "\n", encoding="utf-8")
+ return status if status >= 0 else 128 - status
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--clean-only", action="store_true")
+ args = parser.parse_args()
+ root = Path.cwd()
+ if args.clean_only:
+ clean_target(root)
+ else:
+ raise SystemExit(run_verify(root, ["mvn", "-B", "--no-transfer-progress", "-s", ".mvn/ci-settings.xml", "clean", "verify"]))
diff --git a/.github/scripts/test_run_verify.py b/.github/scripts/test_run_verify.py
new file mode 100644
index 0000000..2f08c31
--- /dev/null
+++ b/.github/scripts/test_run_verify.py
@@ -0,0 +1,62 @@
+"""Synthetic report-provenance tests; these do not stand in for Maven tests."""
+import importlib.util
+import os
+from pathlib import Path
+import sys
+import tempfile
+import unittest
+from unittest.mock import patch
+
+spec = importlib.util.spec_from_file_location("run_verify", Path(__file__).with_name("run_verify.py"))
+verify = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(verify)
+
+
+class ReportProvenanceTest(unittest.TestCase):
+ def exercise(self, exit_code, generate):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ for path in ["target/surefire-reports/TEST-old.xml", "target/site/jacoco/jacoco.xml"]:
+ old = root / path
+ old.parent.mkdir(parents=True, exist_ok=True)
+ old.write_text("old Mac / Jackson 2.17 report")
+ output = root / "outputs"
+ command = [sys.executable, "-c", (
+ "from pathlib import Path; "
+ "assert not Path('target').exists(); "
+ + ("p=Path('target/surefire-reports/TEST-current.xml'); p.parent.mkdir(parents=True); p.write_text(''); "
+ "p=Path('target/site/jacoco/jacoco.xml'); p.parent.mkdir(parents=True); p.write_text(''); " if generate else "")
+ + f"raise SystemExit({exit_code})"
+ )]
+ with patch.dict(os.environ, {"GITHUB_OUTPUT": str(output), "GITHUB_SHA": "current-sha"}):
+ self.assertEqual(exit_code, verify.run_verify(root, command))
+ expected = str(generate).lower()
+ self.assertEqual(f"surefire={expected}\njacoco={expected}\n", output.read_text())
+ self.assertFalse((root / 'target/surefire-reports/TEST-old.xml').exists())
+ for path in (root / 'target').rglob('*'):
+ if path.is_file():
+ self.assertNotIn('Jackson 2.17', path.read_text())
+ if generate:
+ self.assertIn('current-sha', (root / 'target/surefire-reports/ci-origin.json').read_text())
+
+ def test_success_uploads_only_current_reports(self):
+ self.exercise(0, True)
+
+ def test_failure_retains_failure_and_current_reports(self):
+ self.exercise(1, True)
+
+ def test_failure_before_tests_does_not_upload_old_reports(self):
+ self.exercise(2, False)
+
+ def test_skipped_project_build_has_no_reports_or_output(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ (root / 'target/surefire-reports').mkdir(parents=True)
+ (root / 'target/surefire-reports/TEST-old.xml').write_text('old Mac / Jackson 2.17')
+ verify.clean_target(root)
+ self.assertFalse((root / 'target').exists())
+ self.assertFalse((root / 'outputs').exists())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 40cfc82..6d42389 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,6 +31,12 @@ jobs:
- name: Checkout source
uses: actions/checkout@v7
+ - name: Remove checkout build artifacts on runner
+ run: python3 .github/scripts/run_verify.py --clean-only
+
+ - name: Test report provenance helper
+ run: python3 .github/scripts/test_run_verify.py
+
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
@@ -65,20 +71,23 @@ jobs:
SOURCES
- name: Build and verify with JaCoCo coverage gate
- run: mvn -B --no-transfer-progress -s .mvn/ci-settings.xml clean verify
+ id: project_verify
+ run: python3 .github/scripts/run_verify.py
- name: Upload JaCoCo coverage report
- if: always()
+ if: ${{ always() && steps.project_verify.outputs.jacoco == 'true' }}
uses: actions/upload-artifact@v7
with:
name: jacoco-coverage
path: target/site/jacoco
+ if-no-files-found: error
retention-days: 14
- name: Upload surefire reports
- if: always()
+ if: ${{ always() && steps.project_verify.outputs.surefire == 'true' }}
uses: actions/upload-artifact@v7
with:
name: surefire-reports
path: target/surefire-reports
+ if-no-files-found: error
retention-days: 14