CVE-2026-116198: The Snipe-IT Field Marked Encrypted That Stored Plaintext
The short version
Snipe-IT is an open source IT asset tracker that lets administrators mark certain custom fields as encrypted, so sensitive notes get scrambled before they touch the database. I found that one API endpoint, the one that creates an asset, had a logic gap: when the caller didn’t hold the permission tied to encrypted fields, the code skipped the encryption step but then wrote the value anyway, in raw plaintext, into a column that the schema and the interface both presented as encrypted. The column meant to be ciphertext quietly held cleartext instead.
This is fixed now. It is tracked as CVE-2026-116198 (Low). It is a data-integrity and encryption-at-rest issue, not a way for the writer to read secrets back.
Some background
Snipe-IT custom fields can be flagged “encrypted.” When a value goes into one, the application is supposed to run it through encryption before storing it, so that anyone reading the raw database, a backup, a replica, a DBA, sees ciphertext rather than the real content. A separate permission, the one that lets a user view encrypted field values, gates who is trusted to work with the decrypted content. The two are meant to move together: if you have the permission, your writes get encrypted; if you don’t, your writes shouldn’t be landing in these fields at all.
The bug was in how one endpoint handled the “you don’t have the permission” case.
What went wrong
The create-asset endpoint looped over each field, and for an encrypted one it checked the permission before encrypting. The problem was what happened on a failed check: nothing. There was no branch to skip the field. So the code fell through and wrote the value regardless, just without having encrypted it:
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
$field_val = Crypt::encrypt($field_val); // only runs if the permission check passes
}
// nothing here for the deny case: no `continue;` to skip the field
}
$asset->{$field->db_column} = $field_val; // writes unconditionally, plaintext if the check failed
When the caller lacked the permission, the encryption call was skipped and the raw value dropped straight into the database as plaintext, in a column everything else in the system treats as ciphertext.
What is notable is that the sibling endpoint, the one that updates an asset, got this exactly right: on a failed check it skipped the field entirely. The non-API paths and the bulk endpoint were also correct. The create endpoint was the single outlier, one missing branch away from matching its neighbors.
If you want the technical framing: this is missing encryption of sensitive data (CWE-311) caused by an always-incorrect control-flow implementation (CWE-670). The code structure guaranteed the wrong outcome in the deny case rather than just risking it.
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 created a user with permission to create assets but without the encrypted-fields permission, then had them create an asset with a value in an encrypted custom field:
curl -X POST http://127.0.0.1:8000/api/v1/hardware \
-H "Authorization: Bearer $DAVE" -H "Content-Type: application/json" \
-d '{"asset_tag":"ENC-001","name":"...","model_id":1,"status_id":4,
"_snipeit_encrypted_note_2":"PLAINTEXT_SECRET_VALUE_42"}'
# 200 success.
Reading the raw database row afterward showed the value sitting there unencrypted:
asset_tag: ENC-001
enc_col: PLAINTEXT_SECRET_VALUE_42 ; raw plaintext, no encryption envelope
And when a privileged user later tried to read that field through the app, the decryption step choked on the plaintext and returned an error string rather than a value:
{ "Encrypted Note": { "value": "Error Decrypting: The payload is invalid." } }
Worth being precise about the limits: the user who wrote the plaintext could not read it back through the app, because the interface redacts encrypted fields to the literal word “ENCRYPTED” for anyone without the permission. So this is not a confidentiality bypass for the writer. It is a broken storage guarantee.
Why this matters
Flagging a field “encrypted” is a promise: whatever lands there is ciphertext at rest, safe to appear in a backup or a replica or in front of a database administrator. This bug broke that promise for any asset created through the API by a user without the encrypted-fields permission. The sensitive value went to disk in the clear, in a column everyone, including the backup tooling and the people reviewing it, trusted to be encrypted. It also quietly corrupted the data: privileged readers got a decryption error instead of the real content, because the stored bytes were never a valid ciphertext to begin with.
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.7.0. It is documented publicly as GHSA-jh4g-98fm-jcqc, assigned CVE-2026-116198.
The fix
The fix makes the create endpoint behave like the update endpoint: when the permission check denies, skip the field entirely instead of writing it:
if ($field->field_encrypted == '1' && ! Gate::allows('assets.view.encrypted_custom_fields')) {
continue; // do not write anything to an encrypted column without the permission
}
Now a caller without the permission can no longer drop plaintext into an encrypted field; the field is simply left alone.
Takeaway
When an if guards a security-sensitive transformation, the branch that matters most is the one that handles failure. Here the “allowed” path encrypted and the “denied” path had no path at all, so control flowed on to the unconditional write and did precisely the wrong thing every time. When a correct version of the same logic already exists a few methods away, as it did in the update endpoint, the divergence is the finding: sibling code paths that handle the same sensitive operation should be structurally identical, and a reviewer comparing them side by side would have spotted the missing branch.
Reported by Brayden Arnold (@builtbybrayden). Snipe-IT is open source at github.com/grokability/snipe-it.