fix: fully mask web secret previews

This commit is contained in:
CJACK
2026-04-26 00:10:59 +08:00
parent 131ca7d398
commit f1ba805173
12 changed files with 123 additions and 24 deletions

View File

@@ -58,14 +58,6 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
for _, acc := range accounts[start:end] {
testStatus, _ := h.Store.AccountTestStatus(acc.Identifier())
token := strings.TrimSpace(acc.Token)
preview := ""
if token != "" {
if len(token) > 20 {
preview = token[:20] + "..."
} else {
preview = token
}
}
items = append(items, map[string]any{
"identifier": acc.Identifier(),
"name": acc.Name,
@@ -75,7 +67,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
"proxy_id": acc.ProxyID,
"has_password": acc.Password != "",
"has_token": token != "",
"token_preview": preview,
"token_preview": maskSecretPreview(token),
"test_status": testStatus,
})
}

View File

@@ -86,3 +86,33 @@ func TestUpdateAccountMetadataPreservesCredentials(t *testing.T) {
t.Fatalf("password should be preserved, got %#v", acc)
}
}
func TestListAccountsMasksTokenPreview(t *testing.T) {
h := newAdminTestHandler(t, `{
"accounts":[{"email":"u@example.com","password":"pwd"}]
}`)
if err := h.Store.UpdateAccountToken("u@example.com", "abcdefgh"); err != nil {
t.Fatalf("seed runtime token: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/admin/accounts?page=1&page_size=10", nil)
rec := httptest.NewRecorder()
h.listAccounts(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
}
var payload map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response failed: %v", err)
}
items, _ := payload["items"].([]any)
if len(items) != 1 {
t.Fatalf("expected 1 item, got %d", len(items))
}
first, _ := items[0].(map[string]any)
if got, _ := first["token_preview"].(string); got != "ab****gh" {
t.Fatalf("expected masked token preview, got %q", got)
}
}

View File

@@ -28,14 +28,6 @@ func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
accounts := make([]map[string]any, 0, len(snap.Accounts))
for _, acc := range snap.Accounts {
token := strings.TrimSpace(acc.Token)
preview := ""
if token != "" {
if len(token) > 20 {
preview = token[:20] + "..."
} else {
preview = token
}
}
accounts = append(accounts, map[string]any{
"identifier": acc.Identifier(),
"name": acc.Name,
@@ -45,7 +37,7 @@ func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
"proxy_id": acc.ProxyID,
"has_password": strings.TrimSpace(acc.Password) != "",
"has_token": token != "",
"token_preview": preview,
"token_preview": maskSecretPreview(token),
})
}
safe["accounts"] = accounts

View File

@@ -1,6 +1,9 @@
package admin
import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
@@ -33,6 +36,53 @@ func TestFieldStringNilToEmpty(t *testing.T) {
}
}
func TestMaskSecretPreviewKeepsOnlyFirstAndLastTwoChars(t *testing.T) {
cases := map[string]string{
"": "",
"a": "*",
"ab": "**",
"abcd": "****",
"abcdef": "ab****ef",
"abc12345": "ab****45",
}
for input, want := range cases {
if got := maskSecretPreview(input); got != want {
t.Fatalf("maskSecretPreview(%q)=%q want %q", input, got, want)
}
}
}
func TestGetConfigMasksAccountTokenPreview(t *testing.T) {
h := newAdminTestHandler(t, `{
"accounts":[{"email":"u@example.com","password":"pwd"}]
}`)
if err := h.Store.UpdateAccountToken("u@example.com", "abcdefgh"); err != nil {
t.Fatalf("seed runtime token: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/admin/config", nil)
rec := httptest.NewRecorder()
h.getConfig(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
}
var payload map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode response failed: %v", err)
}
accounts, _ := payload["accounts"].([]any)
if len(accounts) != 1 {
t.Fatalf("expected 1 account, got %d", len(accounts))
}
first, _ := accounts[0].(map[string]any)
if got, _ := first["token_preview"].(string); got != "ab****gh" {
t.Fatalf("expected masked token preview, got %q", got)
}
}
func TestRunAccountTestsConcurrentlyKeepsInputOrder(t *testing.T) {
accounts := []config.Account{
{Email: "a@example.com"},

View File

@@ -46,6 +46,17 @@ func nilIfZero(v int64) any {
return v
}
func maskSecretPreview(secret string) string {
secret = strings.TrimSpace(secret)
if secret == "" {
return ""
}
if len(secret) <= 4 {
return strings.Repeat("*", len(secret))
}
return secret[:2] + "****" + secret[len(secret)-2:]
}
func toStringSlice(v any) ([]string, bool) {
arr, ok := v.([]any)
if !ok {