Juju: In-Memory Token Store for Discharge Tokens Lacks Concurrency Safety and Persistence

Description

Summary

The localLoginHandlers struct in the Juju API server maintains an in-memory map to store discharge tokens following successful local authentication. This map is accessed concurrently from multiple HTTP handler goroutines without any synchronization primitive protecting it. The absence of a mutex or equivalent mechanism means that concurrent reads, writes, and deletes on the map can trigger Go runtime panics and may allow a discharge token to be consumed more than once before deletion completes.

Details

When a user authenticates through the local login flow, a discharge token is generated and stored in a plain map[string]string field named userTokens. The form handler writes to this map when authentication succeeds, and the third-party caveat checker reads from and deletes from the same map when a discharge request arrives. Both code paths execute inside goroutines dispatched by the HTTP server, meaning concurrent requests will access the map simultaneously.

Go's runtime detects concurrent map access and will terminate the process with a fatal error when a write races with another write or read. This makes the API server susceptible to a denial-of-service attack from any authenticated user who can trigger simultaneous discharge requests. Beyond the crash scenario, the read-then-delete sequence in the caveat checker is not atomic. Two goroutines processing the same token concurrently may both pass the existence check before either executes the deletion, allowing a single-use discharge token to be accepted more than once and effectively replaying authentication.

The struct definition that introduces the unsafe field is shown below.

type localLoginHandlers struct {
    authCtxt   *authContext
    userTokens map[string]string
}

The concurrent access originates from the caveat checker calling username, ok := h.userTokens[tokenString] followed by delete(h.userTokens, tokenString) with no lock held, while formHandler concurrently executes h.userTokens[token] = username in a separate goroutine.

PoC

package main

import (
    "net/http"
    "sync"
)

func main() {
    token := "acquired-discharge-token"
    endpoint := "https://target-juju-api:17070/local-login/discharge"

    var wg sync.WaitGroup
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            req, _ := http.NewRequest("GET", endpoint+"?token="+token, nil)
            http.DefaultClient.Do(req)
        }()
    }
    wg.Wait()
}

Impact

Any authenticated user who obtains a valid discharge token can send a burst of concurrent requests to the discharge endpoint. The most reliable outcome is a Go runtime panic caused by concurrent map access, which terminates the Juju API server process and denies service to all connected clients and agents. Under favorable timing conditions the same token may be accepted by multiple goroutines before deletion, bypassing the single-use enforcement and allowing repeated authentication with a token that should have been invalidated after first use.

Basic information

Type
reviewed
Severity
medium
Advisory on GitHub
Open advisory ↗
Repository advisory
Open repository advisory ↗
Source code
Browse source ↗
Published (advisory)
2026-04-10 21:00:35 UTC
Updated
2026-04-27 16:21:11 UTC
GitHub reviewed
2026-04-10 21:00:35 UTC
NVD published
2026-04-10

EPSS Score

Score Percentile
0.01% 1.46%

CVSS Scores

Base score Version Severity Vector
6.4 3.1
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H Click to expand
Attack vector (AV:N)
Could be attacked over the internet or any normal routed network—not just someone sitting at the machine.
Attack complexity (AC:H)
Even with access, the exploit needs extra luck, timing, or a fussy environment to actually work.
Privileges required (PR:L)
A normal user session is enough; they don’t have to be admin.
User interaction (UI:N)
Nobody has to click “OK” or open a trap file; it can work without a victim helping.
Scope (S:U)
Damage stays in the same “trust bubble” as the broken component—no big spill into unrelated systems.
Confidentiality (C:L)
Some sensitive info could get out, but not a total data dump.
Integrity (I:L)
Attackers could change some data, but it’s limited—not everything goes.
Availability (A:H)
Could take the service down hard or make it unusable for people who depend on it.
6.1 4.0
CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:L/VI:L/VA:H/SC:N/SI:N/SA:N Click to expand
Attack vector (AV:N)
Could be attacked over the internet or any normal routed network.
Attack complexity (AC:H)
Exploitation depends on constrained or hard-to-reproduce conditions.
Attack requirements (AT:P)
Additional preconditions must be present for exploitation.
Privileges required (PR:L)
Low privileges are required.
User interaction (UI:N)
No user interaction is required.
Vulnerable system confidentiality impact (VC:L)
Limited confidentiality impact on the vulnerable system.
Vulnerable system integrity impact (VI:L)
Limited integrity impact on the vulnerable system.
Vulnerable system availability impact (VA:H)
High availability impact on the vulnerable system.
Subsequent system confidentiality impact (SC:N)
No confidentiality impact on subsequent systems.
Subsequent system integrity impact (SI:N)
No integrity impact on subsequent systems.
Subsequent system availability impact (SA:N)
No availability impact on subsequent systems.

Identifiers

CWEs

CWE id Name
CWE-362 Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')

Credits

  • fg0x0 (reporter)
  • wallyworld (remediation_developer)
  • tlm (remediation_reviewer)

Affected packages (1)

Vulnerable version ranges and first patched releases as published by GitHub.

Ecosystem Package Vulnerable range First patched Vulnerable functions
go github.com/juju/juju < 0.0.0-20260408003526-d395054dc2c3 0.0.0-20260408003526-d395054dc2c3

References

cvelogic Threat Intelligence