fix(security): 移除上游密钥派生片段

This commit is contained in:
tbphp committed 2026-07-29 12:51:11 +08:00
1 parent f9dc0d1ff4
commit 201617b893
24 files changed
+153 -73

No files matched your search

+6
View File
@@ -14,3 +14,9 @@ The repository's 2.0 candidate receives security fixes while its first public re
Report suspected vulnerabilities through [GitHub Private Vulnerability Reporting](https://github.com/tbphp/gpt-load/security/advisories/new).
Please do not open a public issue for an undisclosed vulnerability. Use the private report so the maintainers can investigate and coordinate a fix before public disclosure.
## Provider credential exposure boundary
Provider keys and any derived fragments, including prefix/suffix masks, must not appear in data-plane response headers or bodies, request logs, or the request-log management API. Request-log attempts identify an upstream key only by its internal `key_id`.
The authenticated Group upstream-key management list is the sole approved exception: it may return the existing mask so an operator can identify a key in the provider console. It remains protected by management authentication and never exposes plaintext credentials.
-2
View File
@@ -53,7 +53,6 @@ type requestLogAttemptResponse struct {
GroupID uint `json:"group_id"`
GroupName string `json:"group_name"`
KeyID uint `json:"key_id"`
KeyMask string `json:"key_mask"`
UpstreamModel string `json:"upstream_model"`
StatusCode int `json:"status_code"`
DurationMs int64 `json:"duration_ms"`
@@ -359,7 +358,6 @@ func mapRequestLogListResponse(page requestlog.Page) (requestLogListResponse, er
GroupID: attempt.GroupID,
GroupName: attempt.GroupName,
KeyID: attempt.KeyID,
KeyMask: attempt.KeyMask,
UpstreamModel: attempt.UpstreamModel,
StatusCode: attempt.StatusCode,
DurationMs: attempt.DurationMs,
+21 -3
View File
@@ -124,7 +124,18 @@ func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
AffinityHit: true,
UsageState: usage.StateNotApplicable,
CostState: pricing.CostStateNotApplicable,
Attempts: nil,
Attempts: []requestlog.Attempt{{
Sequence: 1,
GroupID: 12,
GroupName: "Primary",
KeyID: 11,
UpstreamModel: "upstream-model",
StatusCode: 200,
DurationMs: 1234,
FailureCategory: telemetry.FailureCategoryOK,
Action: telemetry.ActionTerminate,
Committed: true,
}},
}},
NextCursor: nextCursor,
},
@@ -176,8 +187,15 @@ func TestRequestLogEndpointReturnsOpaqueCursorAndSafeDTO(t *testing.T) {
t.Fatalf("first response envelope = %#v", envelope)
}
attempts, ok := envelope.Data.Items[0]["attempts"].([]any)
if !ok || len(attempts) != 0 {
t.Fatalf("attempts = %#v, want []", envelope.Data.Items[0]["attempts"])
if !ok || len(attempts) != 1 {
t.Fatalf("attempts = %#v, want one", envelope.Data.Items[0]["attempts"])
}
attempt, ok := attempts[0].(map[string]any)
if !ok || attempt["key_id"] != float64(11) {
t.Fatalf("attempt = %#v, want key_id 11", attempts[0])
}
if _, exists := attempt["key_mask"]; exists {
t.Fatalf("attempt exposes key_mask: %#v", attempt)
}
for _, forbidden := range []string{"headers", "body", "url"} {
if strings.Contains(strings.ToLower(recorder.Body.String()), forbidden) {
@@ -23,7 +23,6 @@ import (
"gpt-load/internal/platform/encryption"
platformhttp "gpt-load/internal/platform/httpclient"
"gpt-load/internal/platform/redact"
"gpt-load/internal/platform/utils"
"gpt-load/internal/protocol"
"gpt-load/internal/state"
"gpt-load/internal/testutil/fakeupstream"
@@ -613,8 +612,8 @@ func TestAnthropicGatewayStream(t *testing.T) {
if err != nil || !strings.Contains(string(rest), "message_stop") || strings.Contains(string(rest), `"code":`) {
t.Fatalf("remaining stream = %q, %v", rest, err)
}
if response.Header.Get(debugHeaderKey) != utils.MaskAPIKey("sk-primary") {
t.Fatalf("debug key = %q", response.Header.Get(debugHeaderKey))
if values := response.Header.Values(debugHeaderKey); len(values) != 0 {
t.Fatalf("debug key = %#v, want absent", values)
}
}
+5 -7
View File
@@ -16,7 +16,6 @@ import (
"gpt-load/internal/dialect"
"gpt-load/internal/health"
"gpt-load/internal/platform/encryption"
"gpt-load/internal/platform/utils"
"gpt-load/internal/ratelimit"
"gpt-load/internal/scheduler"
"gpt-load/internal/state"
@@ -30,8 +29,9 @@ const (
fixedCooldown = time.Minute
blacklistFailureThreshold = 3
debugHeaderGroup = "X-GPTLoad-Group"
debugHeaderKey = "X-GPTLoad-Key"
debugHeaderAttempts = "X-GPTLoad-Attempts"
// debugHeaderKey remains reserved so an upstream cannot inject it downstream.
debugHeaderKey = "X-GPTLoad-Key"
debugHeaderAttempts = "X-GPTLoad-Attempts"
)
var debugHeaderNames = []string{
@@ -385,7 +385,7 @@ func (handler *Handler) executeAttempts(
}
attempts++
updateDebugHeaders(ginContext.Writer.Header(), selection.Group.Name, apiKey, attempts)
updateDebugHeaders(ginContext.Writer.Header(), selection.Group.Name, attempts)
selectedKeyID := selection.KeyID
input := ForwardInput{
Dialect: selectedDialect, Group: selection.Group, APIKey: apiKey, Request: parsed,
@@ -549,13 +549,11 @@ func (handler *Handler) executeAttempts(
func initializeDebugHeaders(headers http.Header) {
headers.Set(debugHeaderGroup, "")
headers.Set(debugHeaderKey, "")
headers.Set(debugHeaderAttempts, "0")
}
func updateDebugHeaders(headers http.Header, group, apiKey string, attempts int) {
func updateDebugHeaders(headers http.Header, group string, attempts int) {
headers.Set(debugHeaderGroup, group)
headers.Set(debugHeaderKey, utils.MaskAPIKey(apiKey))
headers.Set(debugHeaderAttempts, strconv.Itoa(attempts))
}
+51 -14
View File
@@ -737,7 +737,7 @@ func TestHandlerInitializesDebugHeadersBeforeValidation(t *testing.T) {
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
assertDebugHeaders(t, recorder.Header(), "", "", "0")
assertDebugHeaders(t, recorder.Header(), "", "0")
if len(forwarder.inputs)+len(forwarder.streamInputs) != 0 {
t.Fatal("validation failure reached upstream forwarder")
}
@@ -882,7 +882,7 @@ func TestHandlerEnforcesModelUTF8ByteLimitBeforeAttempt(t *testing.T) {
if response.Code != test.wantCode {
t.Fatalf("response code = %q, want %q", response.Code, test.wantCode)
}
assertDebugHeaders(t, recorder.Header(), "", "", "0")
assertDebugHeaders(t, recorder.Header(), "", "0")
if event.ClientModel != "" || len(event.Attempts) != 0 ||
strings.Contains(event.ErrorSummary, test.model) {
t.Fatalf("rejected model leaked into RequestLog event: %#v", event)
@@ -943,7 +943,7 @@ func TestHandlerServesLocalModelEndpoints(t *testing.T) {
t.Fatalf("response = %d %s", recorder.Code, recorder.Body.String())
}
assertJSONEqual(t, recorder.Body.String(), test.expected)
assertDebugHeaders(t, recorder.Header(), "", "", "0")
assertDebugHeaders(t, recorder.Header(), "", "0")
})
}
}
@@ -960,7 +960,7 @@ func TestHandlerModelEndpointsApplyFiltersAndKeepEmptyShape(t *testing.T) {
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
assertJSONEqual(t, recorder.Body.String(), `{"object":"list","data":[{"id":"alpha","object":"model","created":1735689600,"owned_by":"gpt-load"}]}`)
assertDebugHeaders(t, recorder.Header(), "", "", "0")
assertDebugHeaders(t, recorder.Header(), "", "0")
})
t.Run("protocol denied keeps official empty envelope", func(t *testing.T) {
@@ -972,7 +972,7 @@ func TestHandlerModelEndpointsApplyFiltersAndKeepEmptyShape(t *testing.T) {
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
assertJSONEqual(t, recorder.Body.String(), `{"object":"list","data":[]}`)
assertDebugHeaders(t, recorder.Header(), "", "", "0")
assertDebugHeaders(t, recorder.Header(), "", "0")
})
}
@@ -988,7 +988,7 @@ func TestHandlerModelEndpointsRequireValidAccessKey(t *testing.T) {
if recorder.Code != http.StatusUnauthorized || !strings.Contains(recorder.Body.String(), reasonInvalidAccessKey.Code) {
t.Fatalf("response = %d %s", recorder.Code, recorder.Body.String())
}
assertDebugHeaders(t, recorder.Header(), "", "", "0")
assertDebugHeaders(t, recorder.Header(), "", "0")
}
}
@@ -1158,6 +1158,44 @@ func (panicReadCloser) Close() error {
return nil
}
func TestHandlerNeverExposesProviderKeyFragmentsInResponseHeaders(t *testing.T) {
const providerKey = "prov-fragment-test-abcdefghwxyz"
forwarder := &scriptedForwarder{results: []UpstreamResult{{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: []byte(`{"ok":true}`),
}}}
engine, _, _ := newHandlerTestRuntime(t, forwarder, providerKey)
request := httptest.NewRequest(
http.MethodPost,
"/v1/chat/completions",
bytes.NewBufferString(`{"model":"gpt-4o"}`),
)
request.Header.Set("Authorization", "Bearer gl-client")
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
if values := recorder.Header().Values(debugHeaderKey); len(values) != 0 {
t.Fatalf("%s = %#v, want absent", debugHeaderKey, values)
}
if recorder.Header().Get(debugHeaderGroup) != "openai" ||
recorder.Header().Get(debugHeaderAttempts) != "1" {
t.Fatalf("debug headers = %#v", recorder.Header())
}
serialized := fmt.Sprint(recorder.Header())
for _, forbidden := range []string{
providerKey,
providerKey[:4],
providerKey[len(providerKey)-4:],
utils.MaskAPIKey(providerKey),
} {
if strings.Contains(serialized, forbidden) {
t.Fatalf("response headers expose provider key fragment %q: %s", forbidden, serialized)
}
}
}
func TestHandlerReportsFinalAttemptInDebugHeaders(t *testing.T) {
tests := []struct {
name string
@@ -1197,11 +1235,7 @@ func TestHandlerReportsFinalAttemptInDebugHeaders(t *testing.T) {
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
last := forwarder.inputs[len(forwarder.inputs)-1]
assertDebugHeaders(t, recorder.Header(), "openai", utils.MaskAPIKey(last.APIKey), tt.wantAttempts)
if strings.Contains(strings.Join(recorder.Header().Values("X-GPTLoad-Key"), ","), last.APIKey) {
t.Fatal("debug header exposed plaintext key")
}
assertDebugHeaders(t, recorder.Header(), "openai", tt.wantAttempts)
})
}
}
@@ -1327,13 +1361,13 @@ func TestHandlerRejectsSpoofedDebugHeaders(t *testing.T) {
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, request)
assertDebugHeaders(t, recorder.Header(), "openai", utils.MaskAPIKey("sk-real-key"), "1")
assertDebugHeaders(t, recorder.Header(), "openai", "1")
}
func assertDebugHeaders(t *testing.T, headers http.Header, group, key, attempts string) {
func assertDebugHeaders(t *testing.T, headers http.Header, group, attempts string) {
t.Helper()
want := map[string]string{
"X-GPTLoad-Group": group, "X-GPTLoad-Key": key, "X-GPTLoad-Attempts": attempts,
debugHeaderGroup: group, debugHeaderAttempts: attempts,
}
for name, value := range want {
values, exists := headers[http.CanonicalHeaderKey(name)]
@@ -1341,6 +1375,9 @@ func assertDebugHeaders(t *testing.T, headers http.Header, group, key, attempts
t.Errorf("%s = %#v (exists=%t), want exactly [%q]", name, values, exists, value)
}
}
if values := headers.Values(debugHeaderKey); len(values) != 0 {
t.Errorf("%s = %#v, want absent", debugHeaderKey, values)
}
}
func TestReadRequestBodyHonorsLimit(t *testing.T) {
-5
View File
@@ -14,7 +14,6 @@ import (
"gpt-load/internal/health"
"gpt-load/internal/platform/redact"
"gpt-load/internal/platform/utils"
"gpt-load/internal/protocol"
"gpt-load/internal/scheduler"
"gpt-load/internal/state"
@@ -136,7 +135,6 @@ func (recorder *requestRecorder) recordAttempt(
)
return recorder.appendAttempt(
selection,
apiKey,
result,
telemetryFailureCategory(decision.Category),
telemetryAction(decision.Action),
@@ -168,7 +166,6 @@ func (recorder *requestRecorder) recordStreamAttempt(
)
return recorder.appendAttempt(
selection,
apiKey,
result,
category,
action,
@@ -185,7 +182,6 @@ func (recorder *requestRecorder) recordStreamAttempt(
func (recorder *requestRecorder) appendAttempt(
selection scheduler.Selection,
apiKey string,
result UpstreamResult,
category telemetry.FailureCategory,
action telemetry.Action,
@@ -206,7 +202,6 @@ func (recorder *requestRecorder) appendAttempt(
GroupID: selection.GroupID,
GroupName: selection.Group.Name,
KeyID: selection.KeyID,
KeyMask: utils.MaskAPIKey(apiKey),
UpstreamModel: selection.UpstreamModelID,
StatusCode: result.StatusCode,
DurationMs: duration.Milliseconds(),
+5 -5
View File
@@ -160,8 +160,8 @@ func TestRequestRecorderBindsUsageToRecordedAttempt(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
sink := &recordingRequestLogSink{}
recorder := newRequestRecorder(sink, "req-usage-bind", time.Unix(100, 0), 9, protocol.OpenAI, func() time.Time { return time.Unix(101, 0) })
recorder.appendAttempt(scheduler.Selection{GroupID: 11, Group: state.GroupView{Name: "first"}, KeyID: 21}, "sk-first", UpstreamResult{}, telemetry.FailureCategoryOK, telemetry.ActionTerminate, "", "", time.Unix(100, 0), time.Unix(100, 0))
second := recorder.appendAttempt(scheduler.Selection{GroupID: 12, Group: state.GroupView{Name: "second"}, KeyID: 22}, "sk-second", UpstreamResult{StatusCode: http.StatusOK}, telemetry.FailureCategoryOK, telemetry.ActionTerminate, "", "", time.Unix(100, 0), time.Unix(100, 0))
recorder.appendAttempt(scheduler.Selection{GroupID: 11, Group: state.GroupView{Name: "first"}, KeyID: 21}, UpstreamResult{}, telemetry.FailureCategoryOK, telemetry.ActionTerminate, "", "", time.Unix(100, 0), time.Unix(100, 0))
second := recorder.appendAttempt(scheduler.Selection{GroupID: 12, Group: state.GroupView{Name: "second"}, KeyID: 22}, UpstreamResult{StatusCode: http.StatusOK}, telemetry.FailureCategoryOK, telemetry.ActionTerminate, "", "", time.Unix(100, 0), time.Unix(100, 0))
recorder.completeResponse(UpstreamResult{StatusCode: http.StatusOK, Usage: test.result}, health.Result{}, "provider", second)
recorder.emit()
@@ -180,7 +180,7 @@ func TestRequestRecorderBindsUsageToRecordedAttempt(t *testing.T) {
func TestRequestRecorderNon2xxKeepsAttributionButNotApplicable(t *testing.T) {
sink := &recordingRequestLogSink{}
recorder := newRequestRecorder(sink, "req-usage-429", time.Unix(100, 0), 9, protocol.OpenAI, func() time.Time { return time.Unix(101, 0) })
index := recorder.appendAttempt(scheduler.Selection{GroupID: 12, Group: state.GroupView{Name: "second"}, KeyID: 22}, "sk-second", UpstreamResult{StatusCode: http.StatusTooManyRequests}, telemetry.FailureCategoryRateLimited, telemetry.ActionRetry, "", "", time.Unix(100, 0), time.Unix(100, 0))
index := recorder.appendAttempt(scheduler.Selection{GroupID: 12, Group: state.GroupView{Name: "second"}, KeyID: 22}, UpstreamResult{StatusCode: http.StatusTooManyRequests}, telemetry.FailureCategoryRateLimited, telemetry.ActionRetry, "", "", time.Unix(100, 0), time.Unix(100, 0))
recorder.completeResponse(UpstreamResult{StatusCode: http.StatusTooManyRequests, Usage: usage.Result{State: usage.StateComplete, Tokens: usage.Tokens{Output: 30}}}, health.Result{Category: health.FailureCategoryRateLimited}, "provider", index)
recorder.emit()
@@ -195,7 +195,7 @@ func TestRequestRecorderInvalidAttemptIndexDoesNotForgeUsageAttribution(t *testi
t.Run(fmt.Sprintf("index_%d", index), func(t *testing.T) {
sink := &recordingRequestLogSink{}
recorder := newRequestRecorder(sink, "req-usage-invalid", time.Unix(100, 0), 9, protocol.OpenAI, func() time.Time { return time.Unix(101, 0) })
recorder.appendAttempt(scheduler.Selection{GroupID: 12, Group: state.GroupView{Name: "second"}, KeyID: 22}, "sk-second", UpstreamResult{}, telemetry.FailureCategoryOK, telemetry.ActionTerminate, "", "", time.Unix(100, 0), time.Unix(100, 0))
recorder.appendAttempt(scheduler.Selection{GroupID: 12, Group: state.GroupView{Name: "second"}, KeyID: 22}, UpstreamResult{}, telemetry.FailureCategoryOK, telemetry.ActionTerminate, "", "", time.Unix(100, 0), time.Unix(100, 0))
recorder.bindUsage(index, usage.Result{State: usage.StateComplete, Tokens: usage.Tokens{Output: 30}}, true)
recorder.emit()
@@ -209,7 +209,7 @@ func TestRequestRecorderInvalidAttemptIndexDoesNotForgeUsageAttribution(t *testi
func TestRequestRecorderDownstreamFailureKeepsBoundUsage(t *testing.T) {
sink := &recordingRequestLogSink{}
recorder := newRequestRecorder(sink, "req-usage-write", time.Unix(100, 0), 9, protocol.OpenAI, func() time.Time { return time.Unix(101, 0) })
index := recorder.appendAttempt(scheduler.Selection{GroupID: 12, Group: state.GroupView{Name: "second"}, KeyID: 22}, "sk-second", UpstreamResult{StatusCode: http.StatusOK}, telemetry.FailureCategoryOK, telemetry.ActionTerminate, "", "", time.Unix(100, 0), time.Unix(100, 0))
index := recorder.appendAttempt(scheduler.Selection{GroupID: 12, Group: state.GroupView{Name: "second"}, KeyID: 22}, UpstreamResult{StatusCode: http.StatusOK}, telemetry.FailureCategoryOK, telemetry.ActionTerminate, "", "", time.Unix(100, 0), time.Unix(100, 0))
result := usage.Result{State: usage.StateComplete, Tokens: usage.Tokens{UncachedInput: 80, CacheRead: 20, Output: 30}}
recorder.completeResponse(UpstreamResult{StatusCode: http.StatusOK, Usage: result}, health.Result{}, "provider", index)
recorder.completeDownstreamWrite(http.StatusOK)
+14 -3
View File
@@ -12,6 +12,7 @@ import (
"github.com/sirupsen/logrus"
"gpt-load/internal/platform/redact"
"gpt-load/internal/platform/utils"
)
func TestGatewayNeverExposesPlaintextKeys(t *testing.T) {
@@ -154,9 +155,19 @@ func assertNoPlaintextSecrets(t *testing.T, recorder *httptest.ResponseRecorder,
t.Helper()
surfaces := []string{recorder.Body.String(), fmt.Sprint(recorder.Header()), logs}
for _, secret := range secrets {
for _, surface := range surfaces {
if strings.Contains(surface, secret) {
t.Fatalf("gateway exposed plaintext secret %q in %q", secret, surface)
if len(secret) <= 16 {
t.Fatalf("test secret %q is too short to verify masked fragments", secret)
}
for _, forbidden := range []string{
secret,
secret[:4],
secret[len(secret)-4:],
utils.MaskAPIKey(secret),
} {
for _, surface := range surfaces {
if strings.Contains(surface, forbidden) {
t.Fatalf("gateway exposed provider key fragment %q in %q", forbidden, surface)
}
}
}
}
+1 -2
View File
@@ -30,7 +30,6 @@ import (
"gpt-load/internal/platform/encryption"
platformhttp "gpt-load/internal/platform/httpclient"
"gpt-load/internal/platform/redact"
"gpt-load/internal/platform/utils"
"gpt-load/internal/protocol"
"gpt-load/internal/state"
"gpt-load/internal/testutil/fakeupstream"
@@ -294,7 +293,7 @@ func TestHandlerStreamingDebugHeadersRejectUpstreamSpoofing(t *testing.T) {
})
recorder := performStreamingRequest(engine)
assertDebugHeaders(t, recorder.Header(), "stream-group", utils.MaskAPIKey("sk-real-stream-key"), "1")
assertDebugHeaders(t, recorder.Header(), "stream-group", "1")
if strings.Contains(recorder.Body.String(), "sk-real-stream-key") || strings.Contains(recorder.Body.String(), "sk-spoofed-plaintext") {
t.Fatalf("stream response leaked a plaintext key: %s", recorder.Body.String())
}
-1
View File
@@ -30,7 +30,6 @@ func mapEvent(
GroupID: attempt.GroupID,
GroupName: attempt.GroupName,
KeyID: attempt.KeyID,
KeyMask: attempt.KeyMask,
UpstreamModel: projectModel(attempt.UpstreamModel),
StatusCode: attempt.StatusCode,
DurationMs: attempt.DurationMs,
-1
View File
@@ -35,7 +35,6 @@ func TestMapEventPersistsCompletedAtZeroUsageAndJSONArray(t *testing.T) {
GroupID: 12,
GroupName: "Anthropic Primary",
KeyID: 34,
KeyMask: "sk-ant-...wxyz",
UpstreamModel: "claude-sonnet-5",
StatusCode: 429,
DurationMs: 800,
+32
View File
@@ -1,6 +1,7 @@
package requestlog
import (
"bytes"
"context"
"encoding/json"
"math"
@@ -61,6 +62,37 @@ func TestDecodeRequestLogRowsPreservesUsageCostAttribution(t *testing.T) {
}
}
func TestDecodeRequestLogRowsIgnoresHistoricalKeyMask(t *testing.T) {
rows := []models.RequestLog{{
ID: "00000000-0000-4000-8000-000000000605",
CreatedAt: time.Date(2026, time.July, 27, 0, 0, 0, 0, time.UTC),
Protocol: string(protocol.OpenAI),
Status: string(telemetry.RequestStatusSuccess),
UsageState: string(usage.StateNotApplicable),
CostState: string(pricing.CostStateNotApplicable),
Attempts: models.JSON(
`[{"sequence":1,"group_id":7,"group_name":"Primary","key_id":11,"key_mask":"prov****safe","upstream_model":"model","status_code":200,"duration_ms":10,"failure_category":"ok","action":"terminate","will_retry":false,"error_code":"","error_summary":"","committed":true}]`,
),
}}
records, err := decodeRequestLogRows(rows)
if err != nil {
t.Fatalf("decodeRequestLogRows() error = %v", err)
}
if len(records) != 1 || len(records[0].Attempts) != 1 {
t.Fatalf("records = %#v, want one historical attempt", records)
}
encoded, err := json.Marshal(records[0].Attempts[0])
if err != nil {
t.Fatalf("marshal decoded attempt: %v", err)
}
for _, forbidden := range [][]byte{[]byte(`"key_mask"`), []byte("prov"), []byte("safe")} {
if bytes.Contains(encoded, forbidden) {
t.Fatalf("decoded attempt retains historical key material %q: %s", forbidden, encoded)
}
}
}
func TestDecodeRequestLogRowsRejectsInvalidUsageCostValues(t *testing.T) {
base := models.RequestLog{
ID: "00000000-0000-4000-8000-000000000602",
-2
View File
@@ -280,7 +280,6 @@ func TestServiceWarningsExcludeEventContentAndThrottle(t *testing.T) {
}
sensitive := testEvent("request-id-must-not-appear")
sensitive.ErrorSummary = "authorization: Bearer sk-sensitive-warning-secret"
sensitive.Attempts[0].KeyMask = "sensitive-key-mask"
service.Emit(sensitive)
service.Emit(sensitive)
@@ -291,7 +290,6 @@ func TestServiceWarningsExcludeEventContentAndThrottle(t *testing.T) {
for _, forbidden := range []string{
sensitive.RequestID,
sensitive.ErrorSummary,
sensitive.Attempts[0].KeyMask,
"sk-sensitive-warning-secret",
} {
if bytes.Contains(output.Bytes(), []byte(forbidden)) {
-1
View File
@@ -272,7 +272,6 @@ func testEvent(id string) telemetry.RequestEvent {
GroupID: 7,
GroupName: "primary",
KeyID: 8,
KeyMask: "sk-...mask",
UpstreamModel: "upstream-model",
StatusCode: 200,
DurationMs: 20,
-1
View File
@@ -29,7 +29,6 @@ type Attempt struct {
GroupID uint `json:"group_id"`
GroupName string `json:"group_name"`
KeyID uint `json:"key_id"`
KeyMask string `json:"key_mask"`
UpstreamModel string `json:"upstream_model"`
StatusCode int `json:"status_code"`
DurationMs int64 `json:"duration_ms"`
-1
View File
@@ -44,7 +44,6 @@ type Attempt struct {
GroupID uint
GroupName string
KeyID uint
KeyMask string
UpstreamModel string
StatusCode int
DurationMs int64
-1
View File
@@ -37,7 +37,6 @@ func TestRequestTelemetryContractUsesExactFieldAllowlist(t *testing.T) {
"GroupID",
"GroupName",
"KeyID",
"KeyMask",
"UpstreamModel",
"StatusCode",
"DurationMs",
-1
View File
@@ -236,7 +236,6 @@ const requestLogPage = {
group_id: group.id,
group_name: group.name,
key_id: 91,
key_mask: 'sk-v****1234',
upstream_model: visualFixtureData.modelName,
status_code: 429,
duration_ms: 842,
+11 -2
View File
@@ -29,7 +29,6 @@ const item = {
group_id: 7,
group_name: 'Primary',
key_id: 11,
key_mask: 'sk-u****safe',
upstream_model: 'gpt-upstream',
status_code: 200,
duration_ms: 40,
@@ -67,7 +66,6 @@ describe('RequestLog resource', () => {
{ ...item, status: 'unknown' },
{ ...item, duration_ms: Number.POSITIVE_INFINITY },
{ ...item, access_key: { ...item.access_key, key: 'plaintext' } },
{ ...item, attempts: [{ ...item.attempts[0], key_mask: 'plaintext' }] },
{ ...item, attempts: [{ ...item.attempts[0], action: 'future_action' }] },
{ ...item, attempts: [{ ...item.attempts[0], secret_token: 'plaintext' }] },
])('rejects an unsafe rendered log field %#j', (unsafe) => {
@@ -76,6 +74,17 @@ describe('RequestLog resource', () => {
)
})
it('rejects a legacy upstream key mask instead of rendering it', () => {
const unsafe = {
...item,
attempts: [{ ...item.attempts[0], key_mask: 'sk-u****safe' }],
}
expect(() => projectRequestLogPage({ items: [unsafe], next_cursor: null })).toThrow(
InvalidResponseError,
)
})
it('keeps drawer selection outside both list identity and transport query', async () => {
const filters = {
status: 'error',
-10
View File
@@ -48,7 +48,6 @@ export interface RequestLogAttemptDto {
group_id: number
group_name: string
key_id: number
key_mask: string
upstream_model: string
status_code: number
duration_ms: number
@@ -95,7 +94,6 @@ export interface RequestLogPageDto {
}
const requestIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
const canonicalMask = /^(?:\*{4}|.{4}\*{4}.{4})$/u
const protocols = ['openai', 'anthropic', 'gemini'] as const
const statuses = ['success', 'error', 'incomplete', 'canceled'] as const
const failureCategories = [
@@ -116,7 +114,6 @@ const attemptFields = [
'group_id',
'group_name',
'key_id',
'key_mask',
'upstream_model',
'status_code',
'duration_ms',
@@ -168,12 +165,6 @@ function projectRequestID(value: unknown): string {
return result
}
function projectMask(value: unknown): string {
const result = projectString(value)
if (!canonicalMask.test(result)) invalidResponse()
return result
}
function projectStatusCode(value: unknown): number {
return projectSafeInteger(value, { minimum: 0, maximum: 999 })
}
@@ -196,7 +187,6 @@ function projectAttempt(value: unknown): RequestLogAttemptDto {
group_id: projectSafeInteger(record.group_id, { minimum: 1 }),
group_name: projectNonBlankString(record.group_name),
key_id: projectSafeInteger(record.key_id, { minimum: 1 }),
key_mask: projectMask(record.key_mask),
upstream_model: projectNonBlankString(record.upstream_model),
status_code: projectStatusCode(record.status_code),
duration_ms: projectSafeInteger(record.duration_ms, { minimum: 0 }),
@@ -23,7 +23,6 @@ function attempt(
group_id: 7,
group_name: 'Historical Group',
key_id: 21,
key_mask: 'sk-u****safe',
upstream_model: 'gpt-upstream',
status_code: 429,
duration_ms: 40,
@@ -52,9 +51,9 @@ function logFixture(overrides: Partial<RequestLogItemDto> = {}): RequestLogItemD
error_summary: '<script data-canary="log-summary">unsafe()</script>',
affinity_hit: false,
attempts: [
attempt(3, { group_name: 'Third Group', key_mask: 'sk-u****thrd' }),
attempt(3, { group_name: 'Third Group' }),
attempt(1),
attempt(2, { group_name: 'Second Group', key_mask: 'sk-u****scnd' }),
attempt(2, { group_name: 'Second Group' }),
],
group_id: 7,
usage_state: 'complete',
@@ -106,7 +105,8 @@ describe('LogDetailDrawer', () => {
),
).toEqual(['log-attempt-1', 'log-attempt-2', 'log-attempt-3'])
expect(bodyElement('[data-test="log-attempt-1"]').textContent).toContain('Historical Group')
expect(bodyElement('[data-test="log-attempt-1"]').textContent).toContain('sk-u****safe')
expect(bodyElement('[data-test="log-attempt-1"]').textContent).toContain('#21')
expect(document.body.textContent).not.toContain('sk-u')
expect(document.body.textContent).toContain(
'<script data-canary="log-summary">unsafe()</script>',
)
+1 -3
View File
@@ -331,9 +331,7 @@ function estimatedCost(log: RequestLogItemDto): string {
</div>
<div>
<dt>{{ t('monitor.logs.drawer.upstreamKey') }}</dt>
<dd>
<code>{{ attempt.key_mask }}</code> · #{{ attempt.key_id }}
</dd>
<dd>#{{ attempt.key_id }}</dd>
</div>
<div>
<dt>{{ t('monitor.logs.drawer.upstreamModel') }}</dt>
-1
View File
@@ -26,7 +26,6 @@ function attemptFixture(sequence: number): RequestLogAttemptDto {
group_id: 7,
group_name: 'Historical Group',
key_id: 21,
key_mask: 'sk-u****safe',
upstream_model: 'gpt-upstream',
status_code: 200,
duration_ms: 100,