ds2api / internal /auth /admin_security_test.go
luckfun233
security: 修复管理员鉴权与配置导出泄露等严重漏洞
c37473d
Raw
History Blame Contribute Delete
7.59 kB
package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"ds2api/internal/config"
)
// stubStore implements AdminConfigReader for testing without pulling in the
// full config store. We intentionally use a struct (not a mock) so the test
// exercises the real auth code paths.
type stubStore struct {
passwordHash string
expireHours int
validAfter int64
}
func (s stubStore) AdminPasswordHash() string { return s.passwordHash }
func (s stubStore) AdminJWTExpireHours() int { return s.expireHours }
func (s stubStore) AdminJWTValidAfterUnix() int64 { return s.validAfter }
func TestEffectiveAdminKeyNeverReturnsHardcodedDefault(t *testing.T) {
// Security: ensure the historical "admin" default is gone. If this test
// ever fails, it means a regression reintroduced a publicly-known default
// credential that would let anyone log in to deployments without
// DS2API_ADMIN_KEY set.
t.Setenv("DS2API_ADMIN_KEY", "")
t.Setenv("DS2API_CONFIG_JSON", "")
stub := stubStore{}
if got := effectiveAdminKey(stub); got != "" {
t.Fatalf("effectiveAdminKey with no env and no password hash must be empty, got %q", got)
}
if got := AdminKey(); got != "" {
t.Fatalf("AdminKey() with no env must be empty, got %q", got)
}
}
func TestVerifyAdminCredentialRejectsEmptyAndDefaultAdmin(t *testing.T) {
// Security: with no admin key configured and no password hash, login
// MUST be impossible — even if an attacker tries "admin" or empty string.
t.Setenv("DS2API_ADMIN_KEY", "")
t.Setenv("DS2API_CONFIG_JSON", "")
stub := stubStore{}
for _, candidate := range []string{"", "admin", "password", "123456", "default"} {
if VerifyAdminCredential(candidate, stub) {
t.Fatalf("VerifyAdminCredential must reject %q when no credentials configured", candidate)
}
}
}
func TestVerifyAdminCredentialRejectsKnownDefaultAdminWithEnvSet(t *testing.T) {
// Security: when DS2API_ADMIN_KEY is set to a strong value, the literal
// "admin" must NOT authenticate.
t.Setenv("DS2API_ADMIN_KEY", "a-strong-and-random-key-12345")
t.Setenv("DS2API_CONFIG_JSON", "")
stub := stubStore{}
if VerifyAdminCredential("admin", stub) {
t.Fatal("VerifyAdminCredential must reject literal \"admin\" when env key is set to something else")
}
if !VerifyAdminCredential("a-strong-and-random-key-12345", stub) {
t.Fatal("VerifyAdminCredential must accept the configured env key")
}
}
func TestJWTSecretNotHardcodedAdminWhenUnconfigured(t *testing.T) {
// Security: when no DS2API_JWT_SECRET, no password hash, and no
// DS2API_ADMIN_KEY are configured, the JWT signing secret must NOT be
// the historical hardcoded "admin". A process-local random secret is
// used instead so attackers cannot forge tokens.
t.Setenv("DS2API_ADMIN_KEY", "")
t.Setenv("DS2API_JWT_SECRET", "")
t.Setenv("DS2API_CONFIG_JSON", "")
secret := jwtSecret(nil)
if secret == "admin" {
t.Fatal("jwtSecret must not fall back to hardcoded \"admin\"")
}
if secret == "" {
t.Fatal("jwtSecret must be non-empty so token signing is non-trivial")
}
// The fallback secret must be process-stable (same value across calls).
if secret != jwtSecret(nil) {
t.Fatal("jwtSecret fallback must be process-stable so minted tokens verify")
}
}
func TestJWTForgedWithAdminSecretFailsVerification(t *testing.T) {
// Security: an attacker who knows the historical default "admin" cannot
// forge a token that verifies when no credentials are configured, because
// the actual signing secret is a process-local random value.
t.Setenv("DS2API_ADMIN_KEY", "")
t.Setenv("DS2API_JWT_SECRET", "")
// Forge a token signed with the historical "admin" secret.
header := map[string]any{"alg": "HS256", "typ": "JWT"}
payload := map[string]any{
"iat": 0,
"exp": 9999999999,
"role": "admin",
}
h, _ := json.Marshal(header)
p, _ := json.Marshal(payload)
msg := rawB64Encode(h) + "." + rawB64Encode(p)
mac := hmac.New(sha256.New, []byte("admin"))
_, _ = mac.Write([]byte(msg))
forgedSig := mac.Sum(nil)
forged := msg + "." + rawB64Encode(forgedSig)
if _, err := VerifyJWT(forged); err == nil {
t.Fatal("forged token signed with hardcoded \"admin\" secret must NOT verify")
}
}
func TestAdminLoginDisabledWithoutCredentials(t *testing.T) {
// Security: end-to-end check that VerifyAdminRequestWithStore rejects
// Bearer tokens when no credentials are configured, regardless of what
// string the attacker supplies.
t.Setenv("DS2API_ADMIN_KEY", "")
t.Setenv("DS2API_CONFIG_JSON", "")
stub := stubStore{}
for _, token := range []string{"admin", "", "Bearer admin", "anything"} {
req := httptest.NewRequest(http.MethodGet, "/admin/config", nil)
req.Header.Set("Authorization", "Bearer "+token)
if err := VerifyAdminRequestWithStore(req, stub); err == nil {
t.Fatalf("VerifyAdminRequestWithStore must reject token %q when no credentials configured", token)
}
}
}
func TestAdminLoginWithConfiguredPasswordHash(t *testing.T) {
// Regression: ensure password-hash based login still works after the
// default-removal change.
t.Setenv("DS2API_ADMIN_KEY", "")
t.Setenv("DS2API_JWT_SECRET", "")
stub := stubStore{passwordHash: HashAdminPassword("super-secret-pw")}
if !VerifyAdminCredential("super-secret-pw", stub) {
t.Fatal("VerifyAdminCredential must accept the configured password")
}
if VerifyAdminCredential("wrong-password", stub) {
t.Fatal("VerifyAdminCredential must reject wrong password")
}
// JWT secret must derive from the password hash, not the empty admin key.
secret := jwtSecret(stub)
if secret != strings.TrimSpace(stub.passwordHash) {
t.Fatalf("jwtSecret must be the password hash when no env secret set, got %q", secret)
}
}
func TestAdminLoginWithEnvKey(t *testing.T) {
// Regression: ensure env-key based login still works after the change.
t.Setenv("DS2API_ADMIN_KEY", "env-key-12345")
t.Setenv("DS2API_JWT_SECRET", "")
stub := stubStore{}
if !VerifyAdminCredential("env-key-12345", stub) {
t.Fatal("VerifyAdminCredential must accept the configured env key")
}
if VerifyAdminCredential("env-key-99999", stub) {
t.Fatal("VerifyAdminCredential must reject wrong env key")
}
}
func TestAdminLoginWithJWTSecretEnv(t *testing.T) {
// Regression: ensure an explicit JWT secret takes precedence and works
// for both signing and verifying.
t.Setenv("DS2API_ADMIN_KEY", "env-key-12345")
t.Setenv("DS2API_JWT_SECRET", "explicit-jwt-secret")
stub := stubStore{}
if jwtSecret(stub) != "explicit-jwt-secret" {
t.Fatalf("jwtSecret must use explicit env value, got %q", jwtSecret(stub))
}
token, err := CreateJWTWithStore(1, stub)
if err != nil {
t.Fatalf("CreateJWTWithStore failed: %v", err)
}
if _, err := VerifyJWTWithStore(token, stub); err != nil {
t.Fatalf("VerifyJWTWithStore failed: %v", err)
}
}
// usingRealStoreHelper wraps a minimal config.Store so we exercise the same
// store code paths used in production. We avoid pulling in the full server.
func TestUsingRealStoreNoCredentials(t *testing.T) {
if os.Getenv("DS2API_RUN_REAL_STORE_TEST") == "" {
t.Skip("skipping real-store test; set DS2API_RUN_REAL_STORE_TEST=1 to run")
}
t.Setenv("DS2API_ADMIN_KEY", "")
t.Setenv("DS2API_JWT_SECRET", "")
t.Setenv("DS2API_CONFIG_JSON", "{}")
store := config.LoadStore()
if !UsingDefaultAdminKey(store) {
t.Fatal("UsingDefaultAdminKey must be true when nothing is configured")
}
if VerifyAdminCredential("admin", store) {
t.Fatal("VerifyAdminCredential must reject \"admin\" with no credentials configured")
}
}