Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The trust-all default is gone, and the GitHub token is narrowed job by job.
EmailClient.configureStartTlsTrustinstalls no socket factory by default, and aGeneralSecurityExceptionnow surfaces asIllegalStateExceptioninstead of being swallowed.- Every workflow starts from
permissions: contents: read. Inrelease.ymlonly the jobs that push getcontents: write/packages: write, each with a comment saying why. - Third-party actions are pinned to commit SHAs with the version as a comment, and
.github/dependabot.ymlkeeps them current.
issue (blocking): With STARTTLS on and no relaxation set, the server certificate's chain is validated but its host name is not, so CodeQL #22 stays open.
openidm-external-email/src/main/java/org/forgerock/openidm/external/email/impl/EmailClient.java:115-117, :126
javax.mail 1.4.7 reads mail.smtp.ssl.checkserveridentity with default false and sets no endpoint identification algorithm, so JSSE checks only the chain, and nothing in the tree sets the flag. A man-in-the-middle with any publicly trusted certificate for a name they control is accepted for host and receives the SMTP AUTH credentials and the mail. The CodeQL predicate (Mail.qll, isInsecureMailPropertyConfig) matches any constant %.socketFactory% key on props, and line 126 still puts one. Only a constant mail.smtp.ssl.checkserveridentity = "true" on the same variable clears it. The predicate is flow-insensitive, so the alert moves from line 96 to line 103 instead of closing. If the check was left off on purpose (relays addressed by IP or localhost), trustedHosts already covers that case.
if (!trustAll && trustedHosts.isEmpty()) {
// validate the chain (JSSE default) and that the certificate was issued for the host
props.put("mail.smtp.ssl.checkserveridentity", "true");
return;
}Pin: in startTlsValidatesTheServerCertificateByDefault, assertThat(props.get("mail.smtp.ssl.checkserveridentity")).isEqualTo("true"); is red at this head. Add a sentence to chap-mail.adoc saying that the certificate must also match host.
issue (blocking): Saving Settings > Email in the Admin UI deletes starttls.trustedHosts and starttls.trustAll from the stored config.
openidm-ui/openidm-ui-admin/src/main/js/org/forgerock/openidm/ui/admin/settings/EmailConfigView.js:134-140, openidm-ui/openidm-ui-admin/src/main/resources/templates/admin/settings/EmailConfigTemplate.html:57
save() merges the form into the config with the shallow _.extend(this.data.config, formData). The form's only STARTTLS field is starttls.enable, so formData.starttls is {enable: true} and replaces the whole stored object. Only auth.password is restored by hand. An operator who follows the behaviour-change note and adds trustedHosts for a self-signed relay loses it on the next UI save, a port change for example. After that every send fails the handshake, and nothing points at the UI.
var formData = form2js("emailConfigForm",".", true);
if (_.has(formData, "starttls")) {
// keep the STARTTLS keys the form does not edit (trustedHosts, trustAll)
formData.starttls = _.extend({}, this.data.config.starttls, formData.starttls);
}
_.extend(this.data.config, formData);suggestion (non-blocking): Document that trustedHosts must contain the configured host exactly as written. A host missing from the list is not validated normally: JavaMail rejects it.
openidm-external-email/src/main/java/org/forgerock/openidm/external/email/impl/EmailClient.java:124, openidm-doc/src/main/asciidoc/integrators-guide/chap-mail.adoc:130
Once the list is set, MailSSLSocketFactory skips the trust-store check for every host. SocketFetcher then accepts only a host found in the list (case-sensitive match) and otherwise throws "Server is not trusted: ". The doc ("accepted without validation") and the constant's Javadoc imply that unlisted hosts get normal validation. With host: "mail.example.com" and trustedHosts: ["MAIL.example.com"] (or an IP), sending fails even though the certificate is valid. One caveat, not run: this bundle imports com.sun.mail.util from jakarta.mail 2.0.2. If OSGi wires javax.mail 1.4.7's SocketFetcher to its own copy of that package, its instanceof check fails and an unlisted host is accepted with no check at all.
String host = props.getProperty("mail.smtp.host");
if (!trustAll && !trustedHosts.contains(host)) {
logger.warn("starttls.trustedHosts {} does not contain the SMTP host {}", trustedHosts, host);
}suggestion (non-blocking): No case sets trustAll and trustedHosts together, so no test covers which one takes precedence (EmailClient.java:120-125).
openidm-external-email/src/test/java/org/forgerock/openidm/external/email/impl/EmailClientTest.java:60-71
If the branches are swapped so that trustedHosts is tested first, all four cases stay green. Yet {trustAll: true, trustedHosts: [...]} then no longer accepts every certificate, which contradicts chap-mail.adoc ("trustAll—when true, accepts any server certificate"). I traced this mutant against the four cases by reading; it was not executed.
@Test
public void startTlsTrustAllWinsOverTrustedHosts() throws Exception {
Properties props = sessionProperties(json(object(
field("host", "smtp.example.com"),
field("starttls", object(field("enable", true), field("trustAll", true),
field("trustedHosts", array("mail.internal")))))));
MailSSLSocketFactory sf = (MailSSLSocketFactory) props.get(SOCKET_FACTORY);
assertThat(sf.isTrustAllHosts()).isTrue();
}Pin: this case goes red when the branches are swapped, because isTrustAllHosts() is false there.
- Set an explicit read-only GITHUB_TOKEN permissions block on the build, deploy and release workflows, elevating only the jobs that need it (wiki/docs push and release tag: contents:write; ghcr push: packages:write) - Pin the third-party docker/* and softprops/action-gh-release actions to commit SHAs - EmailClient: stop trusting every SMTP server certificate over STARTTLS; validation is now the default, with opt-in starttls.trustedHosts / starttls.trustAll settings (documented) Resolves CodeQL alerts #711-#716, #718-#738, #920, #921 (actions) and OpenIdentityPlatform#22 (java/insecure-smtp-ssl).
Keeps the commit-hash-pinned third-party actions in .github/workflows up to date: Dependabot bumps the SHA and the trailing version comment together, grouped into one weekly PR. Ported from OpenIdentityPlatform/OpenIG#170.
- Validate the SMTP certificate's host name by default (mail.smtp.ssl.checkserveridentity=true); JavaMail 1.4.7 leaves it off - Exclude jakarta.mail from the email bundle's classpath: compiling against its com.sun.mail.util made the bundle import MailSSLSocketFactory from jakarta.mail, which javax.mail's SocketFetcher does not recognise, so starttls.trustedHosts trusted every host - Warn when starttls.trustedHosts does not contain the SMTP host - Admin UI: keep starttls.trustedHosts/trustAll when saving the Email form - Tests: host check, trustAll precedence, socket factory origin - Docs: host match, exact trustedHosts entry, trustAll precedence
fdaf9db to
1fe86c6
Compare
|
@maximthomas all four points are addressed in 1fe86c6 (the branch is also rebased onto the current 1. Host name check. With STARTTLS on and no relaxation set, 2. Admin UI. 3.
4. Precedence. I added
|
The com.sun.mail.util [1.4,2) import from the previous commit left openidm-external-email unresolved, so OpenIDM never reached "ready" in CI: javax.mail 1.4.7 imports its own com.sun.mail.* packages with no upper bound, Felix wires them to jakarta.mail 2.0.2 and drops the 1.4.7 exports. The same wiring already broke mail on master, where MimeMessage fails with NoSuchMethodError on com.sun.mail.util.PropUtil. - Embed javax.mail 1.4.7 in the bundle so Session, SMTPTransport, SocketFetcher and MailSSLSocketFactory come from one jar, and point the context class loader at the bundle while JavaMail loads its providers. - Set mail.smtp.ssl.protocols to the JVM defaults when STARTTLS is on. Without it JavaMail 1.4.7 enables only TLSv1, which current JDKs disable, so no STARTTLS handshake could succeed.
|
@maximthomas a correction to point 3 of my previous reply, in 68c17fb. What went wrong. The The same wiring already breaks mail on Fix.
Verified at runtime. I ran a local SMTP server with STARTTLS and a self-signed certificate for
Startup log is clean against the CI pattern. Outside OSGi, on the same javax.mail jar, I also checked |
Summary
Closes 30 of the 31 open medium CodeQL alerts: the 29 GitHub Actions findings and
java/insecure-smtp-ssl#22. The remaining one (#4,ResourceServletredirect) touches a file that #202 is already changing and will follow once that PR lands.GitHub Actions
actions/missing-workflow-permissions(#711–#716, #920, #921) — every workflow now starts frompermissions: contents: read; only the jobs that actually use the token get more, each with an inline comment saying why:build.yml(all jobs)contents: readregistry:2service need nothing elsedeploy.yml/deploy-mavencontents: writegithub.token(the doc-site push uses a PAT)release.yml/release-mavencontents: writerelease:preparepushes the release tag,action-gh-releasecreates the release, docs go to the wikirelease.yml/release-docker*contents: read,packages: writeGITHUB_TOKENactions/unpinned-tag(#718–#738) — the six third-party actions are pinned to commit SHAs, with the resolved version kept as a comment:docker/metadata-actionv6.2.0 ·docker/setup-qemu-actionv4.4.0 ·docker/setup-buildx-actionv4.4.1 ·docker/build-push-actionv7.4.0 ·docker/login-actionv4.6.0 ·softprops/action-gh-releasev3.0.3actions/*andgithub/*are not covered by the rule and stay on major tags.Second commit adds
.github/dependabot.yml(ported from OpenIdentityPlatform/OpenIG#170) with thegithub-actionsecosystem, grouped into one weekly PR, so the pinned SHAs stay current. All six pins above were verified against the actions' latest releases at the time of writing and are already up to date.#22 —
EmailClienttrusted every SMTP certificate over STARTTLSThe "temporary hack to avoid cert check" installed a
MailSSLSocketFactorywithsetTrustAllHosts(true)wheneverstarttls.enablewas set, so the TLS upgrade gave no protection against an on-path attacker. By default the certificate must now chain to the JVM trust store and be issued for the configuredhost(mail.smtp.ssl.checkserveridentity=true, which JavaMail 1.4.7 leaves off). Two optional settings relax it (documented in the integrator's guide):Once
trustedHostsis set, JavaMail accepts only ahostfound in the list (case-sensitive) and rejects any other, soEmailClientlogs a warning when the configuredhostis missing from it.javax.mail is embedded in the bundle (review rounds 1–2).
bundle/ships bothjavax.mail:mail:1.4.7andcom.sun.mail:jakarta.mail:2.0.2, and both containcom.sun.mail.*. The javax.mail bundle imports its owncom.sun.mail.*packages withversion="1.4"and no upper bound, so Felix wires them to jakarta.mail 2.0.2 and drops the 1.4.7 exports. This already breaks mail onmaster:MimeMessagefails withNoSuchMethodErroroncom.sun.mail.util.PropUtil, andexternal/email?_action=sendreturns 500. Before this fix,MailSSLSocketFactorycame from jakarta.mail, so javax.mail'sSocketFetcherdid not recognise it, and a non-emptytrustedHostswould have trusted every host. Nowopenidm-external-emailembeds javax.mail 1.4.7 (Embed-Dependency) and imports neitherjavax.mailnorcom.sun.mail, soSession,SMTPTransport,SocketFetcherandMailSSLSocketFactoryall come from one jar. While JavaMail looks up providers,EmailClientpoints the context class loader at the bundle. jakarta.mail is excluded from this module's classpath and still ships inbundle/through the other modules.STARTTLS protocols: with no
mail.smtp.ssl.protocolsset, JavaMail 1.4.7 enables onlyTLSv1on the STARTTLS socket, and current JDKs disable it, so no handshake could succeed.EmailClientnow sets the property to the JVM's default protocols.Admin UI: saving Settings > Email now keeps
starttls.trustedHosts/starttls.trustAll, which the form does not edit. Before, the shallow merge replaced the storedstarttlsobject with{enable}.Behaviour change: deployments that use STARTTLS against an SMTP server with a self-signed or otherwise untrusted certificate, or with a certificate that does not match
host(for example a relay addressed by IP), will fail to send mail until they either fix the certificate / trust store or settrustedHosts/trustAll.Test plan
EmailClientTest(7): default → no custom socket factory and host check on;trustAll→ trust-all factory;trustedHosts→ limited factory, no host check;trustAllwins overtrustedHosts; STARTTLS uses the JVM's default TLS protocols;MailSSLSocketFactorycomes from the same jar asjavax.mail.Session; no STARTTLS → nothing configured. The host-check, protocols and socket-factory-origin cases fail without the fixesopenidm-external-emailsuite green (12/12)mail-1.4.7.jar(Bundle-ClassPath: .,mail-1.4.7.jar) and imports neitherjavax.mailnorcom.sun.maillocalhost: startup reaches "ready" with a clean log;trustedHosts: ["localhost"]delivers over TLS; a host missing fromtrustedHostsis rejected ("Server is not trusted") and the warning is logged; default settings reject the certificate. The same install with themasterbundle returns 500 (NoSuchMethodError)trustAlldelivers; a case-mismatchedtrustedHostsentry is rejected; with the test CA trusted,localhostdelivers and127.0.0.1is rejected by the host name checkEmailConfigView.save()merge logic checked in isolation; the Admin UI was not built locally, so lint is left to CIcontents: write/packages: writeare sufficient (the token had full default permissions before)