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) fmt.Printf("Error finding executable: %v\n", err)
os.Exit(1) 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 package gateway
import ( import (
"encoding/json" "crypto/subtle"
"net/http" "net/http"
"strings" "strings"
) )
@ -29,7 +29,7 @@ func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
} }
token := authHeader[len(prefix):] token := authHeader[len(prefix):]
if token != apiKey { if subtle.ConstantTimeCompare([]byte(token), []byte(apiKey)) != 1 {
writeJSONError(w, http.StatusForbidden, "invalid token") writeJSONError(w, http.StatusForbidden, "invalid token")
return return
} }
@ -39,7 +39,5 @@ func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
} }
func writeJSONError(w http.ResponseWriter, code int, message string) { func writeJSONError(w http.ResponseWriter, code int, message string) {
w.Header().Set("Content-Type", "application/json") writeJSON(w, code, map[string]string{"error": message})
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": message})
} }

View file

@ -3,10 +3,10 @@ package gateway
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/KarakuriAgent/clawdroid/pkg/config" "github.com/KarakuriAgent/clawdroid/pkg/config"
"github.com/KarakuriAgent/clawdroid/pkg/logger"
) )
// handleGetSchema returns the configuration schema. // 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{}) { func writeJSON(w http.ResponseWriter, code int, v interface{}) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code) w.WriteHeader(code)
json.NewEncoder(w).Encode(v) if err := json.NewEncoder(w).Encode(v); err != nil {
} logger.ErrorCF("gateway", "failed to encode JSON response", map[string]interface{}{"error": err.Error()})
}
// 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]
} }

View file

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

View file

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