{"id":"GHSA-6vgg-xhvh-38ff","aliases":["GO-2026-5191"],"status":"published","title":"nebula-mesh: POST /api/v1/hosts/{id}/mobile-bundle response lacks Cache-Control: no-store","description":"`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(), \u0026yamlData); err != nil {\n```","severity":{"version":"4.0","score":2.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N","level":"low","from":"ghsa"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-525"],"published":"2026-06-12T18:30:09Z","modified":"2026-06-25T19:56:40Z","fixes":[{"ecosystem":"Go","package":"github.com/juev/nebula-mesh","fixed_in":["0.3.2"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Go","package":"github.com/juev/nebula-mesh","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.3.1"},{"fixed":"0.3.2"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/juev/nebula-mesh","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.2"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/juev/nebula-mesh","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.3.2"}]}]}],"references":[{"url":"https://github.com/juev/nebula-mesh/security/advisories/GHSA-6vgg-xhvh-38ff","tags":["web","advisory"]},{"url":"https://github.com/forgekeep/nebula-mesh/commit/c13d5b2c013b4b323bc0c87a6ecc6afba6384ee5","tags":["web"]},{"url":"https://github.com/advisories/GHSA-6vgg-xhvh-38ff"},{"url":"https://github.com/juev/nebula-mesh","tags":["package"]}],"sources":{"ghsa":[{"id":"GHSA-6vgg-xhvh-38ff","title":"nebula-mesh: POST /api/v1/hosts/{id}/mobile-bundle response lacks Cache-Control: no-store","description":"`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(), \u0026yamlData); err != nil {\n```","status":"published","published":"2026-06-12T18:30:09Z","modified":"2026-06-12T18:30:11Z","cvss":[{"version":"4.0","score":2.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N","level":"low"}],"cwes":["CWE-525"],"updated_at":"2026-11-02T14:40:26.12116Z"}],"osv":[{"id":"GHSA-6vgg-xhvh-38ff","title":"nebula-mesh: POST /api/v1/hosts/{id}/mobile-bundle response lacks Cache-Control: no-store","description":"`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(), \u0026yamlData); err != nil {\n```","status":"published","published":"2026-06-12T18:30:09Z","modified":"2026-06-25T19:56:40.5938772Z","cvss":[{"version":"4.0","score":2.3,"vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N","level":"low"}],"cwes":["CWE-525"],"updated_at":"2026-11-02T15:46:40.308826Z"},{"id":"GO-2026-5191","title":"nebula-mesh: POST /api/v1/hosts/{id}/mobile-bundle response lacks Cache-Control: no-store in github.com/juev/nebula-mesh","description":"nebula-mesh: POST /api/v1/hosts/{id}/mobile-bundle response lacks Cache-Control: no-store in github.com/juev/nebula-mesh","status":"published","published":"2026-06-25T18:43:15Z","modified":"2026-06-25T19:45:15.220715078Z","updated_at":"2026-11-02T15:47:02.556545Z"}]}}
