hscontrol/api: return a minimal 401 for failed security requirements

ogen's SecurityError message echoes the operation name and internal
security text. Map it to a clean 401 so the unauthorized body stays small
and leaks nothing.
This commit is contained in:
Kristoffer Dalby
2026-06-18 07:26:40 +00:00
parent c1c54f14b6
commit a9d5ec6202
2 changed files with 41 additions and 0 deletions
+9
View File
@@ -10,6 +10,7 @@ import (
"github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/state"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/ogen-go/ogen/ogenerrors"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
@@ -96,6 +97,14 @@ func classify(err error) *oas.ErrorStatusCode {
return esc
}
// A failed security requirement (missing/malformed bearer) must not echo
// ogen's internal "operation X: security ...: not satisfied" message, which
// leaks the operation name. Return a minimal 401.
var secErr *ogenerrors.SecurityError
if errors.As(err, &secErr) {
return apiError(http.StatusUnauthorized, "valid API key required")
}
var coder interface{ Code() int }
if errors.As(err, &coder) {
return apiError(coder.Code(), err.Error())
+32
View File
@@ -0,0 +1,32 @@
package apiv1
import (
"errors"
"net/http"
"strings"
"testing"
"github.com/ogen-go/ogen/ogenerrors"
)
var errSecurityNotSatisfied = errors.New(
`operation ListUsers: security "": security requirement is not satisfied`,
)
// TestClassifySecurityErrorIsMinimal ensures a failed security requirement
// becomes a clean 401 that does not leak ogen's internal operation/security
// message.
func TestClassifySecurityErrorIsMinimal(t *testing.T) {
secErr := &ogenerrors.SecurityError{Err: errSecurityNotSatisfied}
esc := classify(secErr)
if esc.StatusCode != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401", esc.StatusCode)
}
detail := esc.Response.Detail.Or("")
if strings.Contains(detail, "operation") || strings.Contains(detail, "ListUsers") {
t.Errorf("401 detail leaks internals: %q", detail)
}
}