Feb 20, 2026·7 min read·13 visits
Cosign forgot to check if the ID card belonged to the person holding it. A logic flaw in the verification code allowed an attacker to present a valid transparency log entry for a benign file while actually delivering a malicious one, tricking the system into marking the malware as 'verified' and 'logged'.
A deep dive into a logic regression in Sigstore Cosign that broke the fundamental promise of transparency logs. By exploiting an early return in the verification routine, attackers could bind a valid Rekor log entry for one artifact to a completely different, malicious artifact, effectively bypassing the 'proof of inclusion' check.
Supply chain security is built on a simple premise: Trust, but verify. Sigstore changed the game by adding a third pillar: Log everything. The idea is that if I sign a binary, that event is recorded in Rekor (the transparency log). When you verify my binary, you don't just check my signature; you check the log to prove I publicly announced this release. It's the digital equivalent of a notary public stamping a document in a book that everyone can see.
But what happens when the notary checks the stamp but forgets to read the document? That is exactly what happened in CVE-2026-22703. For a specific set of configurations involving 'Trusted Roots' (which, ironically, are the more secure way to configure Sigstore), Cosign stopped acting like a strict border guard and started acting like a tired club bouncer who just glances at the ID without looking at the face.
This vulnerability is a 'binding bypass.' In cryptography, binding is everything. A signature must be bound to a message. A certificate must be bound to an identity. A transparency log entry must be bound to the specific artifact it represents. CVE-2026-22703 severed that last link, allowing an attacker to perform a digital bait-and-switch that renders the transparency log—the core feature of the ecosystem—useless for validation.
The root cause of this vulnerability isn't a buffer overflow or a complex heap grooming technique. It is the silent killer of logic: the Early Return. In software engineering, we often use early returns (guard clauses) to clean up code. 'If input is null, return error.' But in security-critical code, flow control is everything. If you return 'Success' too early, you skip the rest of the exam.
The vulnerability resides in pkg/cosign/verify.go, specifically within the VerifyBundle function. This function has two jobs: verify the Rekor entry's signature (authenticity) and ensure the Rekor entry matches the artifact (binding). The regression was introduced when handling TrustedMaterial—a newer abstraction for managing root keys.
When TrustedMaterial was present, the code successfully verified that the Rekor entry was signed by the Rekor service. It validated the timestamp and the TLOG signature. And then... it just stopped. It returned nil (no error). It completely skipped the subsequent lines of code responsible for calling compareSigs, comparePublicKey, and verifying the bundleHash against the payloadHash. It verified the container, but ignored the contents.
Let's look at the logic flow to understand how egregious this skip was. The vulnerable code effectively did this:
// Vulnerable Logic (Conceptual)
func VerifyBundle(ctx, bundle, trustedMaterial, artifact) error {
// Step 1: Check if the log entry is signed by Rekor
if trustedMaterial != nil {
// Check the Signed Entry Timestamp (SET)
if err := verifySET(bundle, trustedMaterial); err != nil {
return err
}
// OOPS: We return here! Success!
return nil
}
// ...
// The code below this line is what actually checks if the bundle
// belongs to the artifact. It is unreachable if trustedMaterial != nil.
// ...
if !compareSigs(bundle, artifact) { return Error }
if !verifyHash(bundle, artifact) { return Error }
return nil
}The patch acts as a harsh corrective measure, hoisting the binding checks to the very top of the function. This ensures that no matter what configuration flags are passed or what new features are added later, the fundamental identity check happens first.
// Patched Logic (Conceptual)
func VerifyBundle(ctx, bundle, trustedMaterial, artifact) error {
// FIX: Verify the binding IMMEDIATELY.
// Does the log entry actually talk about THIS artifact?
if !compareSigs(bundle, artifact) { return Error }
if !verifyHash(bundle, artifact) { return Error }
// Now we can worry about who signed the log entry
if trustedMaterial != nil {
return verifySET(bundle, trustedMaterial)
}
// ...
}By moving the binding verification before the trusted material check, the developers closed the loop. It’s a textbook example of why 'fail-safe defaults' and 'defense in depth' are difficult to maintain as codebases grow and refactor.
To exploit this, we don't need to crack the Rekor private key. We just need to be a lawful user of the system who decides to break the rules. The attack is a Substitution Attack. Here is how a malicious actor, let's call him 'Mallory', pulls this off.
First, Mallory compiles a benign application, hello-world.bin. He signs it with his valid key and submits it to the Rekor transparency log. Rekor responds with a bundle: a signed entry saying 'I, Rekor, witness that Mallory signed hash X at time T.'
Next, Mallory compiles his malware, ransomware.bin. He signs this malware with his same valid key. Now he has a valid signature for the malware. But if he uploads this to Rekor, security researchers might spot it in the public log. So, he doesn't upload it.
Instead, Mallory creates a Frankenstein bundle. He takes the signature from the malware, but he attaches the Rekor bundle from the benign application.
When the victim runs cosign verify --trusted-root=... ransomware.bin, Cosign looks at the bundle. It sees a valid signature on the binary (from Mallory). It then checks the Rekor entry. It asks, 'Is this a valid Rekor entry?' Yes, it is (it's the entry for hello-world.bin). Because of the bug, Cosign stops there. It never asks, 'Does this Rekor entry hash match the ransomware.bin hash?'
The victim installs the ransomware, believing it was properly logged and vetted in the transparency system.
The CVSS score of 5.5 (Medium) feels deceptively low here, likely dragged down by the complexity of the required configuration (specifically using TrustedMaterial roots) and the requirement for the attacker to have a valid signing key (or a compromised one). However, the implication is severe.
The entire value proposition of Sigstore is that it makes software supply chains observable. If you can bypass the log verification, you turn Sigstore back into a standard PKI system, stripping away the 'transparency' protection against key compromise. If a developer's key is stolen, the attacker can sign malware and hide it from the public log by reusing old, valid log entries. The public audit trail—the mechanism designed to catch exactly this scenario—becomes blind to the attack.
For organizations relying on Cosign policy controllers in Kubernetes (e.g., ensuring all pods are logged in Rekor), this bypass means untracked images could be deployed into production environments while appearing fully compliant.
The remediation is straightforward: Update Cosign. The patch is available in versions 2.6.2 and 3.0.4. If you are running a vulnerable version, you are flying blind.
If you cannot update immediately, there is a configuration-based workaround. The vulnerability is triggered specifically when using the modern TrustedMaterial path (often invoked via --trusted-root or TUF). If you manually specify the Rekor public key using the legacy method, you can force the code into the older execution path which—ironically—did not have this bug.
> [!TIP]
> Workaround: Set the SIGSTORE_REKOR_PUBLIC_KEY environment variable to the path of the Rekor public key. This bypasses the TrustedMaterial logic branch entirely in the affected versions.
Ultimately, this is a lesson in code paths. When refactoring for new features (like TUF support), legacy verification logic must be rigorously audited to ensure 'obvious' checks aren't implicitly dropped.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
sigstore/cosign Sigstore | < 2.6.2 | 2.6.2 |
sigstore/cosign Sigstore | >= 3.0.0, < 3.0.4 | 3.0.4 |
| Attribute | Detail |
|---|---|
| CWE | CWE-345 (Insufficient Verification of Data Authenticity) |
| CVSS v3.1 | 5.5 (Medium) |
| Attack Vector | Local / Context-Dependent |
| Impact | Integrity Bypass / Supply Chain Poisoning |
| Exploit Status | PoC Available (Theoretical) |
| GHSA ID | GHSA-whqx-f9j3-ch6m |
The software does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.