API / internal /logging /request_logger_fuzz_test.go
sshinmen's picture
Trigger rebuild on HF Space
e9f4ef2
Raw
History Blame Contribute Delete
10.2 kB
// Package logging provides request logging functionality for the CLI Proxy API server.
// This file contains fuzz tests for the StreamingSanitizer functionality.
package logging
import (
"bytes"
"strings"
"testing"
)
// FuzzSanitizeBytes tests the sanitizeBytes function with various inputs
// to ensure it correctly redacts sensitive fields without corrupting data.
func FuzzSanitizeBytes(f *testing.F) {
// Seed corpus with various input patterns
f.Add([]byte(`{"thinking": "some secret thought"}`))
f.Add([]byte(`{"arguments": "secret args"}`))
f.Add([]byte(`{"thinking": "thought", "arguments": "args"}`))
f.Add([]byte(`{"message": {"thinking": "nested thought"}}`))
f.Add([]byte(`{}`))
f.Add([]byte(`[]`))
f.Add([]byte(``))
f.Add([]byte(`{"thinking": ""}`))
f.Add([]byte(`{"thinking": "multi\nline\ncontent"}`))
f.Add([]byte(`{"thinking": "escaped \"quotes\" here"}`))
f.Add([]byte(`{"other": "thinking: not redacted"}`))
f.Add([]byte(`{"thinking": "value", "other": "data", "arguments": "more"}`))
f.Fuzz(func(t *testing.T, data []byte) {
result := sanitizeBytes(data)
// Ensure result doesn't contain actual thinking values
resultStr := string(result)
// Check that "thinking" values are redacted
// The regex matches: "thinking": "..."
if strings.Contains(resultStr, `"thinking"`) {
// If thinking key exists, its value should be [REDACTED]
// This is a basic check - the actual value should be replaced
if strings.Contains(resultStr, `"thinking": "`) &&
!strings.Contains(resultStr, `"thinking": "[REDACTED]"`) {
// Check if it's not just the key with empty value or special patterns
// Extract the value after "thinking":
idx := strings.Index(resultStr, `"thinking"`)
if idx >= 0 {
afterKey := resultStr[idx+len(`"thinking"`):]
afterKey = strings.TrimLeft(afterKey, " \t\n\r")
if strings.HasPrefix(afterKey, `:`) {
afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r")
if strings.HasPrefix(afterColon, `"`) {
// It's a string value, check if properly redacted
endQuote := strings.Index(afterColon[1:], `"`)
if endQuote > 0 {
value := afterColon[1 : endQuote+1]
if value != "[REDACTED]" && value != "" {
t.Errorf("thinking value not redacted: got %q", value)
}
}
}
}
}
}
}
// Check that "arguments" values are redacted
if strings.Contains(resultStr, `"arguments"`) {
if strings.Contains(resultStr, `"arguments": "`) &&
!strings.Contains(resultStr, `"arguments": "[REDACTED]"`) {
idx := strings.Index(resultStr, `"arguments"`)
if idx >= 0 {
afterKey := resultStr[idx+len(`"arguments"`):]
afterKey = strings.TrimLeft(afterKey, " \t\n\r")
if strings.HasPrefix(afterKey, `:`) {
afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r")
if strings.HasPrefix(afterColon, `"`) {
endQuote := strings.Index(afterColon[1:], `"`)
if endQuote > 0 {
value := afterColon[1 : endQuote+1]
if value != "[REDACTED]" && value != "" {
t.Errorf("arguments value not redacted: got %q", value)
}
}
}
}
}
}
}
// Ensure the result doesn't contain the original input's sensitive values
// (unless they happen to be "[REDACTED]")
inputStr := string(data)
if strings.Contains(inputStr, `"thinking"`) {
// Extract thinking value from input
thinkingVal := extractJSONStringValue(inputStr, "thinking")
if thinkingVal != "" && thinkingVal != "[REDACTED]" {
if strings.Contains(resultStr, thinkingVal) && !strings.Contains(dataStrWithoutKey(resultStr, "thinking"), thinkingVal) {
t.Errorf("original thinking value %q still present in output", thinkingVal)
}
}
}
if strings.Contains(inputStr, `"arguments"`) {
argsVal := extractJSONStringValue(inputStr, "arguments")
if argsVal != "" && argsVal != "[REDACTED]" {
if strings.Contains(resultStr, argsVal) && !strings.Contains(dataStrWithoutKey(resultStr, "arguments"), argsVal) {
t.Errorf("original arguments value %q still present in output", argsVal)
}
}
}
// Ensure result is valid (doesn't panic, is non-nil)
if result == nil && len(data) > 0 {
t.Error("sanitizeBytes returned nil for non-empty input")
}
})
}
// FuzzSanitizeBytesStructured specifically tests structured JSON inputs
func FuzzSanitizeBytesStructured(f *testing.F) {
// Valid JSON with thinking field
f.Add([]byte(`{
"model": "gpt-4",
"thinking": "This is my internal reasoning process",
"messages": [{"role": "user", "content": "Hello"}]
}`))
// Valid JSON with arguments field
f.Add([]byte(`{
"function": "calculate",
"arguments": "{\"x\": 10, \"y\": 20}",
"other": "data"
}`))
// Both fields
f.Add([]byte(`{
"thinking": "secret thought",
"arguments": "secret args",
"output": "public result"
}`))
// Nested thinking (should not be matched by current regex)
f.Add([]byte(`{"nested": {"thinking": "nested value"}}`))
// Array with thinking objects
f.Add([]byte(`[{"thinking": "first"}, {"thinking": "second"}]`))
f.Fuzz(func(t *testing.T, data []byte) {
result := sanitizeBytes(data)
// Basic sanity checks
if len(result) > len(data)*2 {
t.Logf("Result significantly larger than input: input=%d, output=%d", len(data), len(result))
}
// Ensure no panics occurred (we got here)
_ = result
})
}
// FuzzSanitizeBytesEdgeCases tests edge cases and boundary conditions
func FuzzSanitizeBytesEdgeCases(f *testing.F) {
// Empty input
// Note: empty input returns nil from regex ReplaceAll, which is valid behavior
// f.Add([]byte{})
// Single character
f.Add([]byte(`{`))
// Just the key names without proper JSON structure
f.Add([]byte(`thinking`))
f.Add([]byte(`arguments`))
// Keys with various whitespace patterns
f.Add([]byte(`{"thinking" : "value"}`))
f.Add([]byte(`{"thinking":"value"}`))
f.Add([]byte(`{"thinking" : "value"}`))
// Escaped quotes in values
f.Add([]byte(`{"thinking": "value with \"quotes\""}`))
// Unicode content
f.Add([]byte(`{"thinking": "日本語テキスト"}`))
f.Add([]byte(`{"thinking": "🎉 emoji test"}`))
// Very long value
longValue := bytes.Repeat([]byte("a"), 10000)
f.Add(append([]byte(`{"thinking": "`), append(longValue, []byte(`"}`)...)...))
// Binary-like content
f.Add([]byte{0x00, 0x01, 0x02, 0x03})
f.Fuzz(func(t *testing.T, data []byte) {
// Should not panic
result := sanitizeBytes(data)
// Result may be nil for empty input (valid behavior from regex ReplaceAll)
// Result should be valid bytes when non-nil
if result != nil {
_ = len(result)
}
})
}
// Helper function to extract string value from JSON-like structure
func extractJSONStringValue(jsonStr, key string) string {
keyPattern := `"` + key + `"`
idx := strings.Index(jsonStr, keyPattern)
if idx < 0 {
return ""
}
afterKey := jsonStr[idx+len(keyPattern):]
afterKey = strings.TrimLeft(afterKey, " \t\n\r")
if !strings.HasPrefix(afterKey, `:`) {
return ""
}
afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r")
if !strings.HasPrefix(afterColon, `"`) {
return ""
}
// Find closing quote (not escaped)
start := 1
for i := start; i < len(afterColon); i++ {
if afterColon[i] == '"' && (i == 0 || afterColon[i-1] != '\\') {
return afterColon[start:i]
}
}
return ""
}
// Helper function to get string without a specific key's value
func dataStrWithoutKey(dataStr, key string) string {
keyPattern := `"` + key + `"`
idx := strings.Index(dataStr, keyPattern)
if idx < 0 {
return dataStr
}
// Return everything before the key
return dataStr[:idx]
}
// TestSanitizeBytesSpecificCases tests specific known patterns
func TestSanitizeBytesSpecificCases(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple thinking",
input: `{"thinking": "secret"}`,
expected: `{"thinking": "[REDACTED]"}`,
},
{
name: "simple arguments",
input: `{"arguments": "secret"}`,
expected: `{"arguments": "[REDACTED]"}`,
},
{
name: "both fields",
input: `{"thinking": "thought", "arguments": "args"}`,
expected: `{"thinking": "[REDACTED]", "arguments": "[REDACTED]"}`,
},
{
name: "with whitespace",
input: `{"thinking" : "secret"}`,
expected: `{"thinking": "[REDACTED]"}`,
},
{
name: "escaped quotes",
input: `{"thinking": "has \"quotes\""}`,
expected: `{"thinking": "[REDACTED]"}`,
},
{
name: "empty value",
input: `{"thinking": ""}`,
expected: `{"thinking": "[REDACTED]"}`,
},
{
name: "no match",
input: `{"other": "thinking: not redacted"}`,
expected: `{"other": "thinking: not redacted"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sanitizeBytes([]byte(tt.input))
if string(result) != tt.expected {
t.Errorf("sanitizeBytes() = %q, want %q", string(result), tt.expected)
}
})
}
}
// TestSanitizeBytesPreservesStructure ensures sanitization doesn't break JSON structure
func TestSanitizeBytesPreservesStructure(t *testing.T) {
inputs := []string{
`{"model": "gpt-4", "thinking": "reasoning", "messages": []}`,
`[{"thinking": "first"}, {"thinking": "second"}]`,
`{"nested": {"thinking": "value"}}`, // Note: current regex doesn't match nested
`{"thinking": "a", "other": "b", "arguments": "c"}`,
}
for _, input := range inputs {
result := sanitizeBytes([]byte(input))
// Basic structural checks
resultStr := string(result)
// Count braces should match (basic JSON validity)
openBraces := strings.Count(resultStr, "{")
closeBraces := strings.Count(resultStr, "}")
if openBraces != closeBraces {
t.Errorf("Mismatched braces in: %s", resultStr)
}
// Count brackets should match
openBrackets := strings.Count(resultStr, "[")
closeBrackets := strings.Count(resultStr, "]")
if openBrackets != closeBrackets {
t.Errorf("Mismatched brackets in: %s", resultStr)
}
// Quotes should be even
quotes := strings.Count(resultStr, `"`)
if quotes%2 != 0 {
t.Errorf("Unmatched quotes in: %s", resultStr)
}
}
}