File size: 10,188 Bytes
bf9e111 e9f4ef2 bf9e111 e9f4ef2 bf9e111 e9f4ef2 bf9e111 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 | // 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)
}
}
}
|