Watch
1
0
Fork
You've already forked RedFlag
0

fix: registration token moves from install URL to X-Registration-Token header (SEC-002)

A token in the query string leaks to shell history, process lists, and
server access logs. The install endpoint now reads X-Registration-Token;
a query-string token is refused with guidance and is never echoed back
or logged. Server-built and web-UI install commands updated (curl -H,
irm -Headers).
This commit is contained in:
Fimeg 2026-06-11 04:25:32 -04:00
commit 1c4b363375
4 changed files with 57 additions and 7 deletions

View file

@ -847,10 +847,20 @@ func (h *DownloadHandler) generateInstallScript(c *gin.Context, platform, baseUR
// Parse agent ID with defense-in-depth priority
agentIDParam := parseAgentID(c)
// Extract registration token from query parameters
registrationToken := c.Query("token")
// Extract registration token from header. Query-string tokens are rejected
// (SEC-002): a token in the URL leaks to process lists, shell history, and
// server access logs. The token value is never echoed back.
registrationToken := c.GetHeader("X-Registration-Token")
if registrationToken == "" {
return "# Error: registration token is required\n# Please include token in URL: ?token=YOUR_TOKEN\n"
if c.Query("token") != "" {
log.Printf("[WARNING] [server] [downloads] install_token_in_query_rejected remote=%s — token passed via URL, refusing (SEC-002)", c.ClientIP())
return "# Error: passing the registration token in the URL is not supported (it leaks to logs and process lists).\n" +
"# Use the header form instead:\n" +
"# curl -sfL -H \"X-Registration-Token: YOUR_TOKEN\" \"<server>/api/v1/install/<platform>\" | sudo bash\n"
}
return "# Error: registration token is required\n" +
"# Pass it via header:\n" +
"# curl -sfL -H \"X-Registration-Token: YOUR_TOKEN\" \"<server>/api/v1/install/<platform>\" | sudo bash\n"
}
// Determine architecture: use ?arch= query param or default to amd64

View file

@ -15,6 +15,41 @@ import (
"github.com/gin-gonic/gin"
)
// TestInstallScriptRejectsQueryToken locks in SEC-002: a registration token
// passed as a URL query parameter must be refused (it leaks to shell history,
// process lists, and access logs) and must never be echoed back.
func TestInstallScriptRejectsQueryToken(t *testing.T) {
gin.SetMode(gin.TestMode)
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate signing key: %v", err)
}
signingService, err := services.NewSigningService(hex.EncodeToString(privateKey))
if err != nil {
t.Fatalf("new signing service: %v", err)
}
handler := NewDownloadHandler("/tmp/redflag-test", &config.Config{}, nil, signingService)
router := gin.New()
router.GET("/api/v1/install/:platform", handler.InstallScript)
req := httptest.NewRequest(http.MethodGet, "/api/v1/install/linux?token=leaked-secret-token", nil)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
body := resp.Body.String()
if strings.Contains(body, "leaked-secret-token") {
t.Fatalf("rejected install script echoes the token back:\n%s", body)
}
if !strings.Contains(body, "X-Registration-Token") {
t.Fatalf("rejection message does not point at the header form:\n%s", body)
}
if strings.Contains(body, "#!/") || strings.Contains(body, "Registering agent") {
t.Fatalf("query-token request still rendered a real install script")
}
}
func TestWindowsInstallScriptUsesResolvedVersionAndRuntimeServerURL(t *testing.T) {
gin.SetMode(gin.TestMode)
@ -31,7 +66,8 @@ func TestWindowsInstallScriptUsesResolvedVersionAndRuntimeServerURL(t *testing.T
router := gin.New()
router.GET("/api/v1/install/:platform", handler.InstallScript)
req := httptest.NewRequest(http.MethodGet, "/api/v1/install/windows?token=test-token", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/install/windows", nil)
req.Header.Set("X-Registration-Token", "test-token")
req.Host = "redflag.example:31336"
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)

View file

@ -110,7 +110,9 @@ func (h *RegistrationTokenHandler) GenerateRegistrationToken(c *gin.Context) {
serverURL = "http://localhost:8080" // Fallback for development
}
}
installCommand := fmt.Sprintf("curl -sfL \"%s/api/v1/install/linux?token=%s\" | sudo bash", serverURL, token)
// SEC-002: token travels in a header, never the URL — query strings land in
// shell history, process lists, and access logs.
installCommand := fmt.Sprintf("curl -sfL -H \"X-Registration-Token: %s\" \"%s/api/v1/install/linux\" | sudo bash", token, serverURL)
response := gin.H{
"token": token,