CVE-2026-116200: When Snipe-IT's Encrypted LDAP Login Quietly Wasn't


The short version

Snipe-IT is an open source IT asset tracker that can authenticate users against a corporate directory over LDAP. When an administrator turns on encryption for that connection, Snipe-IT asks the directory to upgrade the link to TLS before sending any passwords. I found that Snipe-IT never checked whether that upgrade actually succeeded. If the handshake failed, or an attacker on the network forced it to fail, Snipe-IT shrugged and sent the directory service-account password, and every user’s password on every login, across the wire in plaintext. To everyone watching the screen, login worked perfectly.

This is fixed now. It is tracked as CVE-2026-116200 (Moderate).

Some background

LDAP is the protocol most organizations use to ask their central directory “is this person who they say they are, and what are they allowed to do.” Because that conversation carries passwords, there is a standard way to encrypt it called StartTLS: the client opens a normal connection and then sends a special request asking to upgrade to TLS before anything sensitive is exchanged. The directory answers yes or no. If it answers no, the client is supposed to refuse to continue, because continuing means sending passwords in the clear.

The entire safety of StartTLS rests on the client actually reading that yes-or-no answer.

What went wrong

Snipe-IT sent the upgrade request and then threw the answer away.

In PHP, the function that performs the upgrade returns true if TLS was negotiated and false if it was not. Snipe-IT called it and ignored the return value, then went straight to binding, the step that transmits credentials:

if ($ldap_use_tls == '1') {
    ldap_start_tls($connection);   // returns false on failure; the result is discarded
}
// ... proceeds to ldap_bind() on the same connection regardless

So if the upgrade failed for any reason, the directory not offering StartTLS, a transient error, or an attacker on the path deliberately answering the upgrade request with “protocol error,” the connection stayed plaintext and Snipe-IT bound over it anyway. The bind still succeeded, so the user saw a normal, successful login. Nothing on screen hinted that the “encrypted” channel had silently fallen back to cleartext.

What made this especially avoidable: the correct pattern was already in the same codebase. Snipe-IT’s own LDAP troubleshooting command checked the return value and threw an error on failure. The main login path was the one consumer that did not.

If you want the technical framing: this is an insecure algorithm downgrade (CWE-757) resulting in cleartext transmission of sensitive information (CWE-319). The negotiated protection could be stripped, and the application never noticed it had been.

How I found it and proved it

I do this kind of research in an isolated lab, never against a live company’s data. I stood up an OpenLDAP container configured without TLS, so any StartTLS request would be refused, and pointed a local Snipe-IT at it with encryption switched on. Then I traced exactly what Snipe-IT’s connection code does, with the return values made visible:

ldap_start_tls($conn)  -> false          ("Unable to start TLS: Protocol error")
ldap_error($conn)      -> "Protocol error"
ldap_bind($conn, ...)  -> true            ; credentials sent in cleartext

The upgrade failed, the error was sitting right there in ldap_error(), and Snipe-IT consumed neither. It bound over the unencrypted connection and reported success. The cleartext-on-the-wire conclusion follows directly from that: the application discarded a false from the upgrade and then transmitted credentials on the same link.

Why this matters

Two credential streams rode on this connection. The LDAP service-account password went out on every directory search. And every end user’s password went out on every interactive login. An attacker positioned anywhere on the path between Snipe-IT and the directory, a shared network segment, a compromised hop, a foothold in the same cloud subnet, could both force the downgrade and harvest everything that followed. Those captured credentials then travel: the service account and user passwords are immediately replayable against any other system they unlock. And because login kept working normally, there was no symptom for an operator to chase.

The feature was off by default and required an administrator to opt into LDAP with TLS. But opting into TLS is the security-conscious choice, and it was exactly the configuration this bug betrayed.

Timeline

I reported this to Snipe-IT’s maintainer, Grokability, on May 2, 2026, as part of a batch of findings against version 8.4.1. It was fixed in version 8.6.3. It is documented publicly as GHSA-wx48-r3qh-jvh5, assigned CVE-2026-116200.

The fix

The fix is a few lines, and it mirrors the pattern already used elsewhere in the project: check the return value, and fail closed if the upgrade did not happen.

if ($ldap_use_tls == '1') {
    if (! ldap_start_tls($connection)) {
        throw new \Exception('Could not establish STARTTLS to '.$ldap_host.': '.ldap_error($connection));
    }
}

Now a stripped or failed TLS handshake surfaces as a connection error instead of silently downgrading to plaintext, which is the correct behavior: a channel that was supposed to be encrypted and isn’t should refuse to carry credentials at all.

Takeaway

When a security step can succeed or fail, ignoring the result is the same as skipping the step. Negotiated protections like StartTLS are opportunistic by design: the server can decline, and an attacker can make it look like the server declined. The only thing standing between “encrypted” and “plaintext” is the client checking the answer and refusing to proceed on a no. Fail closed, not open, and when a correct implementation already exists elsewhere in your own codebase, every caller should be using it.


Reported by Brayden Arnold (@builtbybrayden). Snipe-IT is open source at github.com/grokability/snipe-it.