
오프라인 체커 for Thymeleaf CVE-2026-40477 / CVE-2026-41901 — 두 CVSS 9.0 SSTI 결함 중 어느 것에 노출되었는지, 그리고 사용 중인 버전 라인에 수정 사항이 있는지(3.0.x: 없음) 알려줍니다.
Thymeleaf CVE-2026-40477 / CVE-2026-41901 오프라인 점검 도구 —— 의존성 제로, 단일 jar, 네트워크 불필요.
CVSS 9.0 의 SSTI 우회 두 건이 나왔고, 9일 만에 수정 버전이 연달아 두 개 나왔습니다. 이 도구는 먼저 "내가 어느 것에 해당하는지"를 답하고, 그다음 더 곤란한 질문에 답합니다:
내 버전 라인에, 올릴 수 있는 수정 버전이 있는가?
3.1.x 의 경우, 있습니다(3.1.5.RELEASE 로 올리면 끝).
3.0.x 및 그 이전 의 경우, 없습니다 —— 그리고 그게 바로 설치 기반이 가장 큰 집단입니다.
두 advisory 의 영향 범위는 OSV 에서 모두 introduced: 0 입니다:
curl -s https://api.osv.dev/v1/vulns/GHSA-r4v4-5mwr-2fwr | jq '.affected[0].ranges'
# [{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.1.4.RELEASE"}]}]
즉, 3.0 라인 전체가 포함되어 있으며, 버전별 목록에는 3.0.15.RELEASE부터 1.0.0까지 모두 나열되어 있습니다.
반면 Maven Central에서 3.0 라인은 3.0.15.RELEASE에서 끝나며, 이후로는 더 이상 릴리스가 없습니다:
curl -s https://repo1.maven.org/maven2/org/thymeleaf/thymeleaf/maven-metadata.xml \
| grep -o '<version>3\.0\.[^<]*</version>' | tail -1
# <version>3.0.15.RELEASE</version>
두 CVE의 수정 버전은 모두 3.1 라인에 있습니다. 따라서 3.0.x 사용자에게는 "버전 번호만 바꾸는" 선택지가 없으며,
오직 상위 메이저 버전 라인으로 마이그레이션하는 방법뿐입니다 — 그리고 3.1은 공식적으로 명시된 호환성 파괴 변경입니다(공식 ChangeLog.txt의 3.1.0.M1 항목에서 하나씩 인용):
- Removed web-API based expression security objects (#request, #response, #session, #servletContext).
- Removed support for Spring 3.x and Spring 4.x.
- Set minimum JDK compatibility level to JDK 8 project-wide (JDK 17 for thymeleaf-spring6).
템플릿에 ${#request.…} / ${#session.…} 를 작성한 경우, 업그레이드하면 바로 오류가 발생하며, 이러한 값 조회는 Controller가 Model에 넣도록 변경해야 합니다.
이는 일정을 잡아야 하는 마이그레이션이지, 버전 번호만 바꾸는 것이 아닙니다.
deps.dev 의 의존성 수(2026-09-04 직접 조회, 누구나 재계산 가능):
| 버전 | 의존성 수 | 해당 항목 | 동일 라인 업그레이드 가능 여부 |
|---|---|---|---|
| 3.0.11.RELEASE | 1124 | 두 항목 | ❌ 3.0 라인에 수정 버전 없음 |
| 3.0.15.RELEASE | 678 | 두 항목 | ❌ 위와 동일, 또한 이 라인의 종점임 |
| 3.0.12.RELEASE | 641 | 두 항목 | ❌ |
| 3.1.3.RELEASE | 826 | 두 항목 | ✅ 3.1.5로 업그레이드 |
| 3.1.2.RELEASE | 677 | 두 항목 | ✅ |
| 3.1.4.RELEASE | 70 | CVE-2026-41901 만 해당 | ✅ |
| 3.1.5.RELEASE | 590 | — | 이미 안전 |
설치 기반이 가장 큰 단일 버전은 3.0.11.RELEASE 로, 3.1 라인 최고인 3.1.3보다 36% 더 높지만 동일 라인 내 출구가 없습니다.
java -jar thymeleaf-check.jar <目录或 jar/war 路径> [--utf8|--gbk]
$ java -jar thymeleaf-check.jar ./target
[CRITICAL] ./target/myapp.war :: BOOT-INF/lib/thymeleaf-3.0.11.RELEASE.jar
构件:thymeleaf 版本 3.0.11.RELEASE(3.0 线,依据: MANIFEST(Implementation-Version))
命中: CVE-2026-40477 + CVE-2026-41901
受影响,且 3.0 线在 Maven Central 上没有任何修复版(该线止于 3.0.15.RELEASE)。
退出码:2 = 有构件所在版本线没有同线修复版 · 1 = 受影响但同线可升 / 判不了 · 0 = 未发现 · 3 = 用法或路径错。
可以直接挂进 CI。
빌드 산출물(target/*.jar, *.war)을 스캔하고, 소스 디렉터리는 스캔하지 마세요.
Thymeleaf는 대부분 spring-boot-starter-thymeleaf를 통해 전이적으로引入되는데, starter 자체는 버전을 포함하지 않습니다 ——
pom에서 Thymeleaf의 버전 번호를 전혀 볼 수 없습니다. 이런 경우 도구는 "pom을 읽어서 판별할 수 없다"고 명확히 말하며, 존재하지 않는 것으로 취급하지 않습니다.
세 가지 식별 근거를 신뢰도 순으로 나열하면:
Implementation-Version —— jar가 재패키징 도구에 의해 이름이 바뀌어도 인식할 수 있습니다.
(Thymeleaf 공식 jar의 META-INF/ 아래에는 MANIFEST.MF만 있고 maven/ 디렉터리가 없으므로, 이에 대해서는 가장 확실한 근거입니다.)pom.propertiesfat jar / war 내부의 BOOT-INF/lib/, WEB-INF/lib/는 하나씩 풀어서 확인하고, 내장 jar의 MANIFEST도 읽습니다.
CVE-2026-40477 | CVE-2026-41901 | |
|---|---|---|
| GHSA | GHSA-r4v4-5mwr-2fwr | GHSA-c9ph-gxww-7744 |
| CVSS | 9.0 | 9.0 |
| 영향 받음 | <= 3.1.3.RELEASE | <= 3.1.4.RELEASE |
| 수정 | 3.1.4.RELEASE | 3.1.5.RELEASE |
| 공개 | 2026-04-15 | 2026-05-04 |
| 공식 원문 | "fails to properly restrict the scope of accessible objects" | "fails to properly neutralize specific constructs", 샌드박스(제한된) 컨텍스트에서 발생 |
두 번째의 영향 범위에는 3.1.4가 포함되므로, 첫 번째 advisory에 따라 3.1.4로 업그레이드한 사람은 두 번째에 해당합니다.
하지만 이를 "공식이 완전히 수정하지 못했다"고 읽지 마세요 —— 두 advisory는 서로 다른 메커니즘을 설명하며,
3.1.4(2026-04-12)와 3.1.5(2026-04-21) 두 공식 버전은 9일 간격이 있고,
이전 수정이 불완전하다고 말한 공식 원문은 없습니다. 이 도구는 사실을 나란히 제시할 뿐, 이 결론을 대신 내리지 않습니다.
RuleTable.java의 모든 숫자는 다시 확인할 수 있습니다:
python tools/gen_rules.py --show
두 개의 1차 소스로 판정표를 대조한다 — OSV의 영향받는 범위와 버전별 목록, Maven Central의
maven-metadata.xml — 맞지 않으면 비영(非零) 종료한다.
세 가지 규칙:
mvn package # JDK 17;target/thymeleaf-check-0.1.0.jar
mvn test # 19 个用例
런타임 의존성 제로는 의도된 것이다: 조사 도구는 어떤 환경에도 바로 넣어 실행할 수 있어야 한다.
Apache-2.0
| 코드 | 의미 |
|---|---|
0 | 스캔을 마쳤고, 모든 파일을 실제로 읽어 들였으며, 조치가 필요한 발견 사항이 없음 |
4 | 읽을 수 없는 파일이 있음 —— zip이 아니거나, 내용이 잘렸거나, 읽기에 실패함 |
🔴 4는 2026-09-09에 추가되었으며, 추가한 이유는 분명히 밝힐 가치가 있다.
그 이전에는 읽을 수 없는 파일이 출력에 한 줄 힌트만 남기고 **종료 코드는 그대로 0**이었다 ——
흔적을 남기는 것은 사람이 보기 위한 것이고, CI와 스크립트가 보는 것은 종료 코드이므로, "나는 그것을 읽지 못했다"가 자동화에서는 "통과"와 같았다.
손상되었거나, 암호화되었거나, 다운로드가 불완전한 jar는 조용히 "문제를 발견하지 못했다"는 한마디로 변해버린다.
"나는 그것을 읽을 수 없다"와 "당신은 안전하다"는 반드시 서로 다른 말이어야 한다.
⚠️ 정당한 빈 jar는 읽을 수 없는 것으로 치지 않는다(22바이트짜리 EOCD 레코드는 진짜 빈 zip이다). 4를 트리거하지 않는다 ——
거짓 경보가 많아지면 진짜 경보를 아무도 보지 않게 된다.
나머지 종료 코드의 의미는 위의 각 판정 등급과 사용 설명을 참조하라.