Jan 29, 2026·7 min read·105 visits
Juju controllers failed to reject macaroons signed with unknown keys. Instead of blocking them, the system asked for 'discharges' (3rd party verification), effectively helping attackers validate their own forged tokens. Fix requires patching to explicitly handle 'key not found' errors.
A logic flaw in Canonical Juju's macaroon validation mechanism allows attackers to bypass cross-model authorization. By presenting a forged macaroon signed with an unknown key, an attacker can trick the controller into entering a discharge workflow that inadvertently validates the forged permissions. This vulnerability turns a cryptographic failure into a trusted credential exchange.
In the world of distributed systems, cookies are boring. Google gave us Macaroons, and they are objectively cooler. Unlike a static API key or a bearer token, a Macaroon is a 'cookie with caveats'. It carries its own authorization logic. You can take a Macaroon that grants admin access and append a caveat that says ...but only on Tuesdays and ...only from IP 10.0.0.1, then hand it off to a worker node. The worker can't remove those caveats because the signature is chained. It's cryptographic delegation at its finest.
Canonical's Juju uses these Macaroons heavily for Cross-Model Relations (CMR). When you have a database in one model (namespace) and a web app in another, they talk via CMR. The permissions for this are baked into Macaroons.
But here is the thing about cryptography: it relies on trust anchors. If I hand you a Macaroon, you need to verify it against a root key you possess. If you don't have the key, the math fails. In a sane world, if the math fails, you throw the request in the trash. In the world of CVE-2026-1237, Juju decided to ask the attacker for clarification instead. It's the digital equivalent of a bouncer seeing a fake ID drawn in crayon and saying, 'This looks weird, go ask the manager to stamp it.'
The vulnerability lies in the apiserver/common/crossmodel/auth.go file, specifically within the checkMacaroons function. This function utilizes the macaroon-bakery library to validate incoming tokens. When a Macaroon is presented, the bakery attempts to retrieve the root key associated with the token's ID to verify the signature.
Here is where the logic went off the rails. If an attacker mints a Macaroon with a random key (one the Controller doesn't have), the bakery library returns an error: macaroon not found in storage. This is the correct behavior from the library. It is effectively saying, 'I have never seen this token before; it is forged or irrelevant.'
However, the Juju controller didn't treat this error as a hard stop. The developers were likely anticipating a different kind of error: caveat verification failures. In the Macaroon protocol, if a token has 'third-party caveats' (conditions that need checking by another service), the verification fails, and the client is told to go get a 'discharge' token.
Juju's code caught the error from the bakery but didn't distinguish between 'I don't know this key' (Security Breach) and 'I need more proof' (Protocol Flow). It defaulted to the latter. The controller saw the failure, assumed it was a discharge requirement, and initiated a handshake to verify the forged caveats. It implicitly trusted the content of a token it couldn't cryptographically verify.
The fix is surprisingly—and perhaps disturbingly—simple. It relies on string matching against an error message. While effective, it highlights how fragile error handling in Go can be if you aren't using typed errors.
Here is the essence of the vulnerable flow in apiserver/common/crossmodel/auth.go before the patch:
// BEFORE: Blindly processing errors
authInfo, err := d.bakery.CheckMacaroons(ctx, macaroons)
if err != nil {
// The code assumes 'err' means we need a discharge
// It does NOT check if the root key was missing completely
return d.handleDischarge(ctx, err, macaroons)
}The patch (PR #21062) introduces a specific check for the macaroon not found in storage string. If the bakery doesn't know the key, the door is slammed shut immediately.
// AFTER: Trust, but verify the error
const rootKeyErrorMessage = "macaroon not found in storage"
authInfo, err := d.bakery.CheckMacaroons(ctx, macaroons)
if err != nil {
// CRITICAL FIX: explicit check for missing keys
if strings.Contains(errors.Details(err), rootKeyErrorMessage) {
logger.Errorf("rejecting forged macaroon: %v", err)
return nil, apiservererrors.ErrPerm
}
// Only proceed to discharge logic if the key was valid
// but caveats were unsatisfied
return d.handleDischarge(ctx, err, macaroons)
}> [!NOTE] > This is a reminder that library error messages are part of your API contract. If the underlying library changes that error string, this security patch could silently regress.
To exploit this, we don't need to steal a key. We just need to make one up. The attack vector is classified as High Complexity (AC:H) because it requires specific network positioning (Adjacent) and the ability to interact with the CMR endpoint, but the logic is straightforward.
Step 1: The Forge
The attacker uses a local Macaroon bakery to mint a new token. They sign it with a random key, let's call it Key_Evil. Inside, they embed caveats claiming they are a privileged user, for example: user == admin or uuid == target-offer-uuid.
Step 2: The Presentation The attacker sends this forged Macaroon to the target Juju controller's API.
Step 3: The Bait
The Juju controller looks up the ID associated with Key_Evil. It finds nothing. The bakery returns macaroon not found. The vulnerable code catches this error. Instead of returning 403 Forbidden, it thinks: "Ah, the user needs to prove these caveats."
Step 4: The Switch
The controller generates a DischargeRequiredError. This is where it gets funny. The controller effectively tells the attacker: "I can't verify this token yet. Go ask the Discharge Service to verify that user == admin."
Step 5: The Loophole The attacker takes this request to the Discharge Service. The service checks the condition. Since the attacker wrote the condition into the forged Macaroon, and the service is just validating if the condition is met (not the validity of the original token root), the discharge is granted.
The attacker combines the original forged Macaroon with the legitimate Discharge token. The Juju controller now accepts the pair, granting access to the cross-model relation. We have successfully turned a database lookup failure into Remote Code Execution (via workload manipulation).
You might notice the CVSS score is a measly 2.1 (Low). Do not let that fool you into thinking this is harmless. The low score comes from the strict requirements of CVSS v4.0 regarding 'Attack Requirements' (AT:P) and 'Network' (AV:A). You generally need to be already inside the network or have some level of access to the Juju environment to fire this off.
However, in a multi-tenant cloud environment where Juju is managing disparate workloads, this is a privilege escalation nightmare. If you are a tenant on a shared controller, you could theoretically leverage this to access offers and services belonging to other tenants that you should strictly be walled off from.
Impacts include:
It is a classic 'Soft underbelly' vulnerability. Hard to reach, but if you poke it, the whole thing doubles over.
Because this is a logic flaw in the compiled Go binary of the Juju controller, there are no config tweaks or firewall rules that can cleanly fix this. You cannot WAF your way out of a Macaroon logic error easily because the payload is opaque.
Immediate Action:
macaroon not found in storage. In a healthy cluster, this should be rare. If you see bursts of this error followed by successful relation hooks, you might have been probed.For developers using Macaroons in their own Go apps: Learn from this. Always check the error type. Do not assume err != nil simply means 'verification pending'. Sometimes it means 'counterfeit'.
CVSS:4.0/AV:A/AC:H/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:L/SI:L/SA:L| Product | Affected Versions | Fixed Version |
|---|---|---|
Juju Canonical | < 3.6 (approx, reliant on PR date) | See Vendor Advisory |
| Attribute | Detail |
|---|---|
| CWE | CWE-672 (Resource After Expiration/Revocation) |
| Attack Vector | Adjacent Network (AV:A) |
| CVSS v4.0 | 2.1 (Low) |
| Privileges Required | Low (PR:L) |
| User Interaction | None (UI:N) |
| Exploit Status | PoC / Theoretical |
The application does not correctly handle the expiration or revocation of a resource, or in this case, the absence of a valid root of trust, allowing continued access.
An uncontrolled resource consumption vulnerability exists in the Docling document conversion library. Maliciously structured HTML, JATS, ODS, or BoxNote inputs containing table cells with excessively large 'rowspan' or 'colspan' attribute values trigger algorithmic complexity conditions. This allows unauthenticated remote attackers to initiate resource exhaustion states, crashing or hanging the target document processing pipeline while bypassing configured timeouts.
A Local File Inclusion (LFI) and Arbitrary File Disclosure vulnerability exists in Docling and Docling Slim versions >= 2.16.0 up to 2.131.0. When parsing serialized DoclingDocument structures using the JSON input format, the backend fails to restrict image URI schemes, allowing remote attackers to retrieve local files and verify path existence on the host system during embedded document export.
Docling, a tool for parsing and processing diverse document formats, is vulnerable to arbitrary file read, arbitrary file write, and potential remote code execution (RCE) in versions 2.94.0 through 2.131.0. The vulnerability occurs when applications configure Docling to use the Tectonic engine for rendering TikZ diagrams into images. Because the compilation did not restrict hazardous TeX primitives or sandbox the environment, an attacker can supply crafted documents containing malicious TikZ definitions to access or modify local files and execute arbitrary commands under the privileges of the processing application.
An SSRF guard bypass vulnerability in the Docling document conversion engine allows unauthenticated attackers to bypass internal IP access controls. The vulnerability exists due to a DNS rebinding Time-of-Check Time-of-Use (TOCTOU) condition, URL authority parsing inconsistencies, and unvalidated network requests triggered during headless browser page rendering.
A technical analysis of CVE-2026-105742 (GHSA-p3fw-7699-7926), a sensitive information disclosure vulnerability in the Docling document processing library. Vulnerable versions of Docling indiscriminately forward custom HTTP headers, such as authentication tokens, to arbitrary third-party origins and during cross-origin redirects while fetching remote image assets from untrusted HTML and EPUB documents.
CVE-2026-106121 is a Denial of Service (DoS) vulnerability in the RabbitMQ Java Client library (amqp-client) affecting versions prior to 5.37.0. The vulnerability resides in the legacy, custom JSON-RPC parsing class com.rabbitmq.tools.json.JSONReader. When parsing malformed or truncated payloads ending within a quoted string or single-line comment, the parser's scanner enters an infinite loop. This occurs because the loop lacks an exit condition for the end-of-input sentinel character returned by the iterator, leading to either CPU exhaustion or a JVM crash from an OutOfMemoryError.