{"id":"GHSA-6vgg-xhvh-38ff","title":"nebula-mesh: POST /api/v1/hosts/{id}/mobile-bundle response lacks Cache-Control: no-store","summary":"nebula-mesh: POST /api/v1/hosts/{id}/mobile-bundle response lacks Cache-Control: no-store","severity":"low","cwe":["CWE-525"],"vendor":"juev","product":"github.com/juev/nebula-mesh","ecosystem":"go","affected":["github.com/juev/nebula-mesh <= 0.3.1"],"patched":["github.com/juev/nebula-mesh 0.3.2"],"published":"2026-06-12","updated":"2026-06-12","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-6vgg-xhvh-38ff","references":[{"url":"https://github.com/juev/nebula-mesh/security/advisories/GHSA-6vgg-xhvh-38ff"},{"url":"https://github.com/forgekeep/nebula-mesh/commit/c13d5b2c013b4b323bc0c87a6ecc6afba6384ee5"},{"url":"https://github.com/advisories/GHSA-6vgg-xhvh-38ff"}],"tags":["ghsa","go"],"ingestedAt":"2026-07-07T15:41:59.219Z","slug":"GHSA-6vgg-xhvh-38ff","body":"## Overview\n\n`internal/api/mobile_bundle.go:62-66` sets only `Content-Type: application/yaml`. The Web-UI sibling at `internal/web/handlers.go:1316-1321` sets `Cache-Control: no-store`, `Pragma: no-cache`, `Expires: 0`, `X-Content-Type-Options: nosniff` — and has a test asserting it. The API path was missed.\n\n## Affected\nAll released versions up to v0.3.0.\n\n## Threat model\nThe endpoint returns a freshly minted X25519 private key inline. Without `no-store`, any intermediary proxy or CDN that caches `200 OK` YAML responses retains the private key for its cache TTL. Same applies to browser disk cache for direct API hits. Combined with the cross-tenant authz advisory (critical), even a corrected authz layer would still leak via cache after fix.\n\n## Suggested fix\nCopy the four headers from the Web sibling:\n\n```go\nw.Header().Set(\"Content-Type\", \"application/yaml; charset=utf-8\")\nw.Header().Set(\"Cache-Control\", \"no-store\")\nw.Header().Set(\"Pragma\", \"no-cache\")\nw.Header().Set(\"Expires\", \"0\")\nw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n```\n\nMirrors `internal/web/handlers.go:1316-1321`. Add a parallel test to the existing web-side coverage.\n\n## Suggested patch\n\nVerified locally: `go vet`, `go test -race -count=1 ./...`, `golangci-lint v2.12` all clean.\n\n```diff\ndiff --git a/internal/api/mobile_bundle.go b/internal/api/mobile_bundle.go\nindex fc09da0..73152eb 100644\n--- a/internal/api/mobile_bundle.go\n+++ b/internal/api/mobile_bundle.go\n@@ -58,8 +58,15 @@ func (s *Server) handleMobileBundle(w http.ResponseWriter, r *http.Request) {\n \t\treturn\n \t}\n \n-\t// Return YAML bundle with proper content-type\n+\t// Return YAML bundle with proper content-type. The bundle inlines a\n+\t// freshly-minted X25519 private key, so suppress every layer of cache\n+\t// between server and operator (intermediate proxies/CDNs, browser disk\n+\t// cache). Mirrors the Web-UI sibling at internal/web/handlers.go.\n \tw.Header().Set(\"Content-Type\", \"application/yaml; charset=utf-8\")\n+\tw.Header().Set(\"Cache-Control\", \"no-store\")\n+\tw.Header().Set(\"Pragma\", \"no-cache\")\n+\tw.Header().Set(\"Expires\", \"0\")\n+\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n \tw.WriteHeader(http.StatusOK)\n \tif _, err := w.Write(bundle); err != nil {\n \t\ts.logger.Error(\"write mobile bundle response\", \"error\", err)\ndiff --git a/internal/api/mobile_bundle_test.go b/internal/api/mobile_bundle_test.go\nindex dcb8cd9..da08b01 100644\n--- a/internal/api/mobile_bundle_test.go\n+++ b/internal/api/mobile_bundle_test.go\n@@ -52,6 +52,19 @@ func TestHandleMobileBundle_Success(t *testing.T) {\n \t\tt.Errorf(\"Content-Type = %q, want 'application/yaml; charset=utf-8'\", ct)\n \t}\n \n+\t// Bundle inlines a private key — every cache between server and operator\n+\t// must drop the response. Mirrors the Web-UI sibling's headers.\n+\tfor header, want := range map[string]string{\n+\t\t\"Cache-Control\":         \"no-store\",\n+\t\t\"Pragma\":                \"no-cache\",\n+\t\t\"Expires\":               \"0\",\n+\t\t\"X-Content-Type-Options\": \"nosniff\",\n+\t} {\n+\t\tif got := w.Header().Get(header); got != want {\n+\t\t\tt.Errorf(\"%s = %q, want %q\", header, got, want)\n+\t\t}\n+\t}\n+\n \t// Verify body is valid YAML with expected keys\n \tvar yamlData map[string]interface{}\n \tif err := yaml.Unmarshal(w.Body.Bytes(), &yamlData); err != nil {\n```\n\n## Affected packages\n\n- `github.com/juev/nebula-mesh <= 0.3.1`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/juev/nebula-mesh 0.3.2`","depth":"sunlit","depthScore":14,"depthScoreParts":{"impact":13.8,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}