Skip to content

Add certificate pinning to address MITM vulnerabilities - #280

Open
mgaffigan wants to merge 6 commits into
OpenIntegrationEngine:mainfrom
mgaffigan:bugfix/client-mitm
Open

mgaffigan wants to merge 6 commits into
OpenIntegrationEngine:mainfrom
mgaffigan:bugfix/client-mitm

Conversation

@mgaffigan

@mgaffigan mgaffigan commented Mar 30, 2026 •

Copy link
Copy Markdown
Contributor

Addresses MITM vulnerability by adding -trust {config} option to client. Adds administrator.pinnedclienttrust option to mirth.properties to control the option.

Option Description Hostname Validation
pki Trust CAs and peer trust based on client JVM config. Normal trust mode. Required (matches CN or SAN)
webserver Trust the auto-generated server's certificate. Not Validated
<thumbprint> Trust a specified SHA-256 certificate thumbprint. Not Validated
insecure_trust_all_certs Trust all certificates without validation (not recommended). Not Validated
localhost Trust all certificates for the hostname localhost Not Validated

If argument is not passed to the client (e.g.: for development without a launcher), pki,localhost is used. If the option is not specified in mirth.properties or for new installs, pki,webserver is used. Multiple options can be specified by separating with commas.

Typical scenarios:

  • Localhost, docker, or direct network access with trusted or self signed certificate
    ✅ No edits needed, secure by default
  • Reverse proxy with trusted certificate
    ✅ No edits needed, secure by default
  • Reverse proxy with self signed certificate
    ⚠️ Edit needed to add the proxy's certificate or enable insecure_trust_all_certs

@github-actions

github-actions Bot commented Mar 30, 2026 •

Copy link
Copy Markdown

Test Results

128 files  + 2  128 suites  +2   3m 40s ⏱️ + 1m 31s
743 tests +26  743 ✅ +26   0 💤 ± 0  0 ❌ ±0 
839 runs  +74  818 ✅ +59  21 💤 +15  0 ❌ ±0 

Results for commit 8e5eda8. ± Comparison against base commit 03eefcf.

♻️ This comment has been updated with latest results.

@mgaffigan
mgaffigan marked this pull request as ready for review March 30, 2026 06:12
@ChristopherSchultz

Copy link
Copy Markdown

Making this a command-line option indicates to me that the server is normally going to be the one calling the shots. IMHO this is not a useful security control, because it relies on the server to enforce it's own trust which, in the case of this PR's attack model, is where the attack originates: the "server" (impersonated by a MitM).

It would be better to make this a GUI-based setup where the client always asks the user to approve a certificate it hasn't seen before, and then (optionally) adds it to a local trust store.

@ChristopherSchultz

Copy link
Copy Markdown

If this PR is merged, the -trust option needs to be able to be specified either multiple times, or allow `fingerprint[,fingerprint...]" syntax because trusting a single certificate isn't good enough for real-world use.

You also need to be able to trust a CA certificate (root or intermediate, not the leaf), which isn't available in the options listed.

I don't understand the webserver trust. How do you know that the cert you are getting from the server is an auto-generated cert from a legitimate OIE server?

@mgaffigan

mgaffigan commented Mar 31, 2026 •

Copy link
Copy Markdown
Contributor Author

@ChristopherSchultz

Making this a command-line option indicates to me that the server is normally going to be the one calling the shots. IMHO this is not a useful security control, because it relies on the server to enforce it's own trust which, in the case of this PR's attack model, is where the attack originates: the "server" (impersonated by a MitM).

The deference to the command line is because the client is not the launcher. I agree with the GUI prompt being required for untrusted certificates, but that has to happen in the launcher. Prompting a second time in the client itself adds no security value.

In the JNLP model, trust has to already been established before executing the first line of downloaded code (otherwise there would be an RCE vulnerability). The only thing the client can do is continue the prior trust established by the launcher.

@mgaffigan

Copy link
Copy Markdown
Contributor Author

@ChristopherSchultz

If this PR is merged, the -trust option needs to be able to be specified either multiple times, or allow `fingerprint[,fingerprint...]" syntax because trusting a single certificate isn't good enough for real-world use.

The current implementation permits any combination of options, separated by commas (including multiple fingerprints).

You also need to be able to trust a CA certificate (root or intermediate, not the leaf), which isn't available in the options listed.

The current implementation trusts any fingerprint provided as long as it is present in the chain provided by the server (root, intermediate, or leaf cert).

I don't understand the webserver trust. How do you know that the cert you are getting from the server is an auto-generated cert from a legitimate OIE server?

The "webserver" option is converted by the server into a specific fingerprint based the actual webserver certificate when generating the JNLP. "webserver" is not a valid option for the client.

@ChristopherSchultz

Copy link
Copy Markdown

The deference to the command line is because the client is not the launcher. I agree with the GUI prompt being required for untrusted certificates, but that has to happen in the launcher. Prompting a second time in the client itself adds no security value.

I agree completely: the launcher needs to have this capability. Adding it to the application, as directed by the server, serves no purpose.

In the JNLP model, trust has to already been established before executing the first line of downloaded code (otherwise there would be an RCE vulnerability). The only thing the client can do is continue the prior trust established by the launcher.

Again, I agree: the launcher needs to first validate the server before downloading anything. Thus, the client cannot be trusted to make these decisions on its own. What you have built here (unless I'm very much misunderstanding) is that the server tells the client (not the launcher) what to trust. If the server has been compromised via MitM, then the client is still vulnerable. It's just vulnerable to a much different kind of attack.

This kind of verification belongs in the launcher and not the client.

@mgaffigan

Copy link
Copy Markdown
Contributor Author

@ChristopherSchultz

Adding it to the application, as directed by the server, serves no purpose.

This PR serves one very specific purpose: to allow the launcher to tell the client which servers it should trust. It allows a passing of the baton from the launcher to the client.

With a secure launcher, but without this PR, an attacker can MITM the server API calls from the client. That results a loss of confidentiality (password disclosure by client to an attacker), and loss of authenticity of server responses (which can probably be used for client RCE via deserialization vulnerability).

The launcher cannot validate API calls to the server; the client must do the validation.

What you have built here is that the server tells the client (not the launcher) what to trust.

The JNLP is a communication to the launcher, not the client. The JNLP must be a trusted document since it specifies what code to download, what class to execute. The launcher root of trust must be in authentication of the JNLP (the server TLS cert, or some signature or hash of the JNLP itself). Anything else allows an attacker to RCE the client by design, by modifying the JNLP.

The server includes the pinned trust list in the JNLP, which is authenticated by the launcher. The client is the ultimate receiver of the trust list, and interprets it.

@ChristopherSchultz

Copy link
Copy Markdown

I see only one useful setting: "use this specific trust store" with a proper certificate in it, having been fetched and approved by the launcher before the client runs.

"Trust any localhost" isn't worth implementing. "Trust anybody" isn't worth implementing. Thumbprints require more code to implement your own trust store. Just use the existing SSLSocketFactory tie-ins that allow you to specify the trust managers. Writing your own trust manager that looks at the fingerprint is very non-Java.

I would drop all the server-specific parts of this PR and all the client stuff with the exception of anything that points it at the correct trust store.

@mgaffigan

mgaffigan commented Apr 2, 2026 •

Copy link
Copy Markdown
Contributor Author

@ChristopherSchultz

"Trust any localhost" isn't worth implementing.

Trust any localhost is intended to be used without a launcher. localhost is by nature not vulnerable to MITM attack, nor loss of confidentiality. This option primarily exists for developers, and is not present in the default configuration.

"Trust anybody" isn't worth implementing.

Trust anybody is intended for use in allowing backwards compatibility with the historical behavior. It is fundamentally insecure if someone modifies their server configuration to use that option, but I believe it is neccessary for practical purposes in a brown-field codebase. That's also why it has the name insecure_trust_all_certs with the word "insecure" in the name.

Thumbprints require more code to implement your own trust store. Just use the existing SSLSocketFactory tie-ins that allow you to specify the trust managers. Writing your own trust manager that looks at the fingerprint is very non-Java.

The "pki" trust option is the "java defaults" trust store. For anyone with the ability to execute in that mode, they are welcome to. There is no mandate to use the trust managers you disagree with.

I would drop all the server-specific parts of this PR and all the client stuff with the exception of anything that points it at the correct trust store.

How do you imagine the launcher should know what servers to trust without a list provided in the JNLP? Are you assuming that the JNLP is served from the singular valid cert? Or some OOB configuration

I'm very open to other designs, but this is the only one I see that is backwards compatible with current launchers, while moving us to better security. If you have an alternate model, send PR's or document a design compatible with the ecosystem of launchers.

@mgaffigan

Copy link
Copy Markdown
Contributor Author

Related kayyagari/ballista#53

@pacmano1

Copy link
Copy Markdown
Contributor

@mgaffigan at least with Launcher which prompts about accepting the server cert (but no longer verfies jars), is this now addressed?

@mgaffigan

Copy link
Copy Markdown
Contributor Author

@mgaffigan at least with Launcher which prompts about accepting the server cert (but no longer verfies jars), is this now addressed?

No. This is still required to address the issue. Both launcher and client are vulnerable.

@pacmano1 pacmano1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes, for two reasons.

The pin can be bypassed. CertificateThumbprintMatcher accepts a match anywhere in the chain the server sends. The server's certificate is public, so a MITM sends its own certificate followed by a copy of the real one and passes both the trust check and the hostname check.

Reproduced against this branch: a client configured with pki,<real thumbprint> connected to a fake server presenting [attackerCert, realServerCert], and the login request, password included, arrived at the fake server. The attacker certificate on its own is rejected. Compare only chain[0].

The CLI can't reach remote engines. CommandLineInterface calls new Client(server), which now defaults to pki,localhost, and the CLI has no way to pass a trust setting. Against an engine using the default self-signed certificate, mirth-cli-launcher.jar can only connect via localhost. From any other address it fails with PKIX path building failed.

java -jar mirth-cli-launcher.jar -a https://<engine-ip>:8443 -u admin -p <password> -v 0.0.0   # fails
java -jar mirth-cli-launcher.jar -a https://localhost:8443 -u admin -p <password> -v 0.0.0      # works

The CLI needs a trust option in the same format as -trust.

The client's connection monitor is a non-daemon thread, and runShell only
closed the client on success.  A refused connection, rejected certificate,
or failed login printed the error and then hung forever.

Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
Nothing covered command end to end.  The harness image now carries the CLI
and runs it as a child process, so the distribution layout and launcher
manifest are covered too.

Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
mgaffigan and others added 4 commits September 26, 2026 02:00
Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
The harness connects to the stack's server at https://oie:8443, whose
certificate is self-signed and generated on first boot under a hostname no
certificate could match. Certificate pinning changed the client default from
"trust any self-signed certificate, never check the hostname" to "pki,localhost",
so every integration test started failing the TLS handshake with a PKIX error.

Give the harness its own trust setting, defaulted to insecure_trust_all_certs.
It lives in HarnessConfig rather than in ci/ so the harness behaves the same in
CI, from Gradle, and in an IDE; -Doie.pinnedClientTrust overrides it for a run
against a real certificate.

No product default changes: HarnessConfig is in the smoke test's own source set
and is only ever packaged into the local CI harness image.

Signed-off-by: Mitch Gaffigan <mitch@intouchpharma.com>
Covers the trust managers end to end against the certificate the server actually
presents, which unit tests cannot do: the certificate is self-signed, generated
on first boot, and served under a hostname it does not match.

ServerCertificate discovers the thumbprint over a throwaway handshake and
recomputes it from the DER encoding, so it is an independent oracle rather than
a second call into the matcher under test.

Three cases: pinning the discovered thumbprint logs in, where the pin is the only
reason the connection can succeed; pinning a thumbprint one character off is
refused, so the comparison is exact rather than merely rejecting garbage; and pki
alone is refused, guarding the regression this suite tripped over.

Failures are asserted as an SSLException anywhere in the cause chain. A rejected
pin currently surfaces as the JDK's "trustAnchors parameter must be non-empty",
since a thumbprint-only configuration defers to an empty trust store, so matching
on a message would be fragile.

Gated to the two derby configurations: the harness container is the same image
everywhere, and only the server image that generated the certificate varies.

Signed-off-by: Mitch Gaffigan <mitch@intouchpharma.com>
Pinning left command on the pki,localhost default, which cannot reach a
remote server with a self-signed certificate.  Adds the same -trust option
the administrator takes, readable from mirth-cli-config.properties.

Signed-off-by: Mitch Gaffigan <mitch.gaffigan@comcast.net>
@mgaffigan

Copy link
Copy Markdown
Contributor Author

@pacmano1, thank you for the review. I've pushed patches to both of those.

@mgaffigan
mgaffigan requested a review from pacmano1 September 26, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants