Harden public URL detection and setup token handling

This commit is contained in:
rcourtman 2025-11-20 19:27:14 +00:00
parent 17d55b8cf4
commit d6cbfc23ec
2 changed files with 82 additions and 13 deletions

View file

@ -185,8 +185,7 @@ func (h *ConfigHandlers) ValidateSetupToken(token string) bool {
defer h.codeMutex.RUnlock()
if code, exists := h.setupCodes[tokenHash]; exists {
// Allow tokens while they are valid or within a short grace period after use.
if now.Before(code.ExpiresAt.Add(2 * time.Minute)) {
if !code.Used && now.Before(code.ExpiresAt) {
return true
}
}
@ -5555,8 +5554,8 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
}
log.Debug().
Str("authToken", req.AuthToken).
Str("authCode", authCode).
Bool("hasAuthToken", strings.TrimSpace(req.AuthToken) != "").
Bool("hasSetupCode", strings.TrimSpace(authCode) != "").
Bool("hasConfigToken", h.config.HasAPITokens()).
Msg("Checking authentication for auto-register")
@ -5578,7 +5577,7 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
// Not the API token, check if it's a temporary setup code
codeHash := internalauth.HashAPIToken(authCode)
log.Debug().
Str("authCode", authCode).
Bool("hasAuthCode", true).
Str("codeHash", codeHash[:8]+"...").
Msg("Checking auth token as setup code")
h.codeMutex.Lock()
@ -5593,10 +5592,9 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
// what's entered in the UI and what's provided in the setup script URL
if setupCode.NodeType == req.Type {
setupCode.Used = true // Mark as used immediately
// Allow the token to be reused for a brief grace period so the setup
// script can complete follow-up actions (temperature verification, etc).
graceExpiry := time.Now().Add(5 * time.Minute)
if setupCode.ExpiresAt.After(graceExpiry) {
// Allow a short grace period for follow-up actions without keeping tokens alive too long
graceExpiry := time.Now().Add(1 * time.Minute)
if setupCode.ExpiresAt.Before(graceExpiry) {
graceExpiry = setupCode.ExpiresAt
}
h.recentSetupTokens[codeHash] = graceExpiry

View file

@ -1524,11 +1524,21 @@ func (r *Router) capturePublicURLFromRequest(req *http.Request) {
return
}
if !canCapturePublicURL(r.config, req) {
return
}
if r.config.EnvOverrides != nil && r.config.EnvOverrides["publicURL"] {
return
}
rawHost := firstForwardedValue(req.Header.Get("X-Forwarded-Host"))
peerIP := extractRemoteIP(req.RemoteAddr)
trustedProxy := isTrustedProxyIP(peerIP)
rawHost := ""
if trustedProxy {
rawHost = firstForwardedValue(req.Header.Get("X-Forwarded-Host"))
}
if rawHost == "" {
rawHost = req.Host
}
@ -1540,9 +1550,12 @@ func (r *Router) capturePublicURLFromRequest(req *http.Request) {
return
}
rawProto := firstForwardedValue(req.Header.Get("X-Forwarded-Proto"))
if rawProto == "" {
rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Scheme"))
rawProto := ""
if trustedProxy {
rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Proto"))
if rawProto == "" {
rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Scheme"))
}
}
scheme := strings.ToLower(strings.TrimSpace(rawProto))
switch scheme {
@ -1668,6 +1681,64 @@ func shouldAppendForwardedPort(port, scheme string) bool {
return true
}
func canCapturePublicURL(cfg *config.Config, req *http.Request) bool {
if cfg == nil || req == nil {
return false
}
if isDirectLoopbackRequest(req) {
return true
}
return isRequestAuthenticated(cfg, req)
}
func isRequestAuthenticated(cfg *config.Config, req *http.Request) bool {
if cfg == nil || req == nil {
return false
}
if cfg.ProxyAuthSecret != "" {
if ok, _, _ := CheckProxyAuth(cfg, req); ok {
return true
}
}
if cfg.HasAPITokens() {
if token := strings.TrimSpace(req.Header.Get("X-API-Token")); token != "" {
if _, ok := cfg.ValidateAPIToken(token); ok {
return true
}
}
if authHeader := strings.TrimSpace(req.Header.Get("Authorization")); strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
if _, ok := cfg.ValidateAPIToken(strings.TrimSpace(authHeader[7:])); ok {
return true
}
}
}
if cookie, err := req.Cookie("pulse_session"); err == nil && cookie.Value != "" {
if ValidateSession(cookie.Value) {
return true
}
}
if cfg.AuthUser != "" && cfg.AuthPass != "" {
const prefix = "Basic "
if authHeader := req.Header.Get("Authorization"); strings.HasPrefix(authHeader, prefix) {
if decoded, err := base64.StdEncoding.DecodeString(authHeader[len(prefix):]); err == nil {
if parts := strings.SplitN(string(decoded), ":", 2); len(parts) == 2 {
if parts[0] == cfg.AuthUser && auth.CheckPasswordHash(parts[1], cfg.AuthPass) {
return true
}
}
}
}
}
return false
}
// handleHealth handles health check requests
func (r *Router) handleHealth(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet && req.Method != http.MethodHead {