fix: improve gateway security and clean up helpers

Use constant-time comparison for auth token validation to prevent
timing attacks. Consolidate writeJSONError into writeJSON, add error
logging for JSON encoding failures, remove unused isSecretKey helper,
and handle syscall.Exec error on restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kohei 2026-02-26 18:07:02 +09:00
parent 7e73df9752
commit fd17e4b174
5 changed files with 23 additions and 34 deletions

View file

@ -590,7 +590,10 @@ func gatewayCmd() {
fmt.Printf("Error finding executable: %v\n", err)
os.Exit(1)
}
syscall.Exec(exe, os.Args, os.Environ())
if err := syscall.Exec(exe, os.Args, os.Environ()); err != nil {
fmt.Printf("Error restarting: %v\n", err)
os.Exit(1)
}
}
}

View file

@ -1,7 +1,7 @@
package gateway
import (
"encoding/json"
"crypto/subtle"
"net/http"
"strings"
)
@ -29,7 +29,7 @@ func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
}
token := authHeader[len(prefix):]
if token != apiKey {
if subtle.ConstantTimeCompare([]byte(token), []byte(apiKey)) != 1 {
writeJSONError(w, http.StatusForbidden, "invalid token")
return
}
@ -39,7 +39,5 @@ func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
}
func writeJSONError(w http.ResponseWriter, code int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": message})
writeJSON(w, code, map[string]string{"error": message})
}

View file

@ -3,10 +3,10 @@ package gateway
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/KarakuriAgent/clawdroid/pkg/config"
"github.com/KarakuriAgent/clawdroid/pkg/logger"
)
// handleGetSchema returns the configuration schema.
@ -134,15 +134,7 @@ func restoreSecrets(incoming, current map[string]interface{}) {
func writeJSON(w http.ResponseWriter, code int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(v)
}
// isSecretKey returns true if the given JSON key should be treated as a secret.
// This is used by the handlers to check whether to mask a value, and is also
// exposed for testing purposes.
func isSecretKey(key string) bool {
// Check the leaf key (last segment after dots)
parts := strings.SplitN(key, ".", -1)
leaf := parts[len(parts)-1]
return secretKeys[leaf]
if err := json.NewEncoder(w).Encode(v); err != nil {
logger.ErrorCF("gateway", "failed to encode JSON response", map[string]interface{}{"error": err.Error()})
}
}

View file

@ -48,8 +48,8 @@ var secretKeys = map[string]bool{
func BuildSchema(defaultCfg *config.Config) SchemaResponse {
var sections []SchemaSection
cfgType := reflect.TypeOf(*defaultCfg)
cfgVal := reflect.ValueOf(*defaultCfg)
cfgType := reflect.TypeOf(defaultCfg).Elem()
cfgVal := reflect.ValueOf(defaultCfg).Elem()
for i := 0; i < cfgType.NumField(); i++ {
field := cfgType.Field(i)

View file

@ -962,22 +962,18 @@ func TestAuthMiddleware_CorrectToken_200(t *testing.T) {
}
}
func TestIsSecretKey(t *testing.T) {
tests := []struct {
key string
want bool
}{
{"api_key", true},
{"gateway.api_key", true},
{"channels.telegram.token", true},
{"model", false},
{"gateway.port", false},
func TestSecretKeys(t *testing.T) {
wantSecret := []string{"api_key", "token", "bot_token", "app_token", "channel_secret", "channel_access_token"}
for _, k := range wantSecret {
if !secretKeys[k] {
t.Errorf("secretKeys[%q] = false, want true", k)
}
}
for _, tc := range tests {
got := isSecretKey(tc.key)
if got != tc.want {
t.Errorf("isSecretKey(%q) = %v, want %v", tc.key, got, tc.want)
notSecret := []string{"model", "host", "port", "enabled"}
for _, k := range notSecret {
if secretKeys[k] {
t.Errorf("secretKeys[%q] = true, want false", k)
}
}
}