Security release. Fixes a server-side request forgery in the store module index fetch, and brings the Cypress suite back to green.
🔒 Security - SSRF egress filter on storeModuleListUrl (JAHIA-SEC-271, CWE-918)
storeModuleListUrl is an OSGi configuration value that the Jahia JVM fetches, and it was passed straight to new URL(...).openConnection() with no scheme, host or address validation.
Because the component declares configurationPolicy = REQUIRE with no @Modified method, Felix SCR deactivates and reactivates it on any write to its .cfg, and activate() unconditionally schedules a refresh on both of its branches - so a configuration write alone became an outbound request from the server, with no further admin action.
The value is now validated before every request, and again on every redirect hop:
Rule
Rejected examples
https scheme only
http://…, file:///etc/passwd, ftp://…
No credentials in the URL
https://admin:secret@host/…
Host must not resolve to a non-routable or internal address
127.0.0.1, [::1], 0.0.0.0, 169.254.169.254 (AWS/GCP/Azure metadata), 10/8, 172.16/12, 192.168/16, 100.64/10 (incl. Alibaba metadata), fd00::/8, fe80::/10, multicast, 240/4
IPv6 transition forms judged by the IPv4 they carry
[::127.0.0.1], [::ffff:0:7f00:1], [64:ff9b::7f00:1], [64:ff9b:1::7f00:1], [2002:7f00:1::], [2001:0:7f00:1::]
Only RFC 3986 host syntax accepted, so parser tricks fail closed
127.1, 0x7f.0.0.1, 127.0.0.1., https://host;@169.254.169.254/
All-digit host labels with a leading zero refused
0177.0.0.1 - decimal to the JVM, octal to an inet_aton proxy
Redirects are not auto-followed
a 302 from an allowed host to 169.254.169.254
A NAT64-wrapped public address is still accepted, so IPv6-only nodes keep reaching the legitimate store. A rejected value is logged and replaced by the shipped default, so update detection degrades to the bundled catalogue rather than failing.
The policy is deliberately not an OSGi property: the write primitive this defends against rewrites that whole file, so an allow-list living in it would defend nothing.
⚠️ Breaking change
If storeModuleListUrl points at an internal mirror - a private IP, localhost, or plain http - it is now rejected and falls back to the public store URL, with an ERROR log line naming the reason. Restore it by setting this on the Jahia JVM, which also permits plain http:
-Djahia.modulemanagement.storeIndex.allowInternalHosts=true
Installations using the default https://store.jahia.com/... URL are unaffected and need no action.
🧪 Tests
- First unit tests in the repo: 66, covering the egress policy and the guarded fetcher, including a replay of the original proof-of-concept paired with a positive control proving an unvalidated fetch does reach the same listener.
- Cypress suite back to green: 137 passing, 0 failing (was 5 failing). Two pre-existing causes, neither related to the security fix - three upload assertions stale since tgz support landed in July, and three dependency-tab tests asserting a tab the module cannot render.
- The test harness now sets its own COMPOSE_PROJECT_NAME, so it no longer shares a Compose namespace with sibling module harnesses.
Known residual gaps
Documented in the class javadoc and the README rather than claimed closed: DNS rebinding (bounded in practice by the JDK's 30 s positive DNS cache and by TLS hostname verification), NAT64 network-specific prefixes which cannot be enumerated, forward proxies resolving hostnames themselves, and the escape hatch not being an absolute boundary.
Full changelog: 1_0_0...1_0_1