Make things 'betterer' across the board (#23)

* Make things 'betterer' across the board

* fix: reorganize struct fields and config parameters for consistency

- [x] Reorder Config struct fields alphabetically and by related functionality
- [x] Reorganize Observation model fields with archival fields grouped together
- [x] Reorder ObservationStore fields to group related members
- [x] Reorder Store struct fields with health check caching grouped
- [x] Reorganize HealthInfo and PoolMetrics struct field order
- [x] Reorder maintenance Service struct fields logically
- [x] Reorganize MCP server handler parameter structs alphabetically
- [x] Reorder pattern detector candidate tracking fields
- [x] Reorganize search Manager struct fields by functionality
- [x] Reorder vector Client struct fields with mutex protections grouped
- [x] Reorganize handler request/response struct fields
- [x] Update handlers_test.go to expect wrapped response format
- [x] Reorder middleware TokenAuth and rate limiter fields
- [x] Reorganize Service struct fields with grouped functionality
- [x] Fix RateLimiter field ordering for clarity
- [x] Reorder CircuitBreaker metrics fields

* fix(security): improve JSON output safety and path traversal protection

- [x] Replace unsafe JSON string formatting with proper json.Marshal in export handler
- [x] Remove escapeJSONString helper function in favor of standard JSON marshaling
- [x] Add safeResolvePath function to validate paths and prevent directory traversal
- [x] Apply path traversal validation in captureFileMtimes operations
- [x] Cap result slice capacity in getRecentSearchQueries to prevent DoS via excessive allocation

* fix(sdk): improve path traversal protection and allocation safety

- [x] Enhance safeResolvePath with stricter validation using filepath.Rel
- [x] Reject paths containing ".." after cleaning to prevent traversal
- [x] Validate absolute paths are within cwd when cwd is specified
- [x] Apply safeResolvePath validation to GetFileContent for consistency
- [x] Add comprehensive test coverage for path traversal protection
- [x] Fix allocation safety in getRecentSearchQueries by using constant capacity
This commit is contained in:
2026-01-11 01:51:20 +00:00
committed by GitHub
parent 3107eddeb2
commit d04b60517a
46 changed files with 12710 additions and 2068 deletions
+204
View File
@@ -974,3 +974,207 @@ func TestSyncSummaryFuncType(t *testing.T) {
}
assert.NotNil(t, fn)
}
// TestSanitizePrompt tests prompt sanitization for CLI safety.
func TestSanitizePrompt(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "normal text",
input: "Hello, world!",
expected: "Hello, world!",
},
{
name: "text with newlines",
input: "Line 1\nLine 2\nLine 3",
expected: "Line 1\nLine 2\nLine 3",
},
{
name: "text with tabs",
input: "Key:\tValue",
expected: "Key:\tValue",
},
{
name: "text with carriage return",
input: "Line 1\r\nLine 2",
expected: "Line 1\r\nLine 2",
},
{
name: "text with null bytes",
input: "Hello\x00World",
expected: "HelloWorld",
},
{
name: "text with control characters",
input: "Hello\x01\x02\x03World",
expected: "HelloWorld",
},
{
name: "text with bell character",
input: "Hello\x07World",
expected: "HelloWorld",
},
{
name: "text with backspace",
input: "Hello\x08World",
expected: "HelloWorld",
},
{
name: "text with form feed",
input: "Hello\x0cWorld",
expected: "HelloWorld",
},
{
name: "text with escape",
input: "Hello\x1bWorld",
expected: "HelloWorld",
},
{
name: "unicode text",
input: "Hello 世界 🌍",
expected: "Hello 世界 🌍",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "only control characters",
input: "\x00\x01\x02\x03",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sanitizePrompt(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestMaxPromptSize tests that MaxPromptSize is reasonable.
func TestMaxPromptSize(t *testing.T) {
assert.Equal(t, 100*1024, MaxPromptSize)
}
// BenchmarkSanitizePrompt benchmarks the sanitize function.
func BenchmarkSanitizePrompt(b *testing.B) {
prompt := "Analyze the following code:\n```go\nfunc main() {\n\tfmt.Println(\"Hello, World!\")\n}\n```\n\nPlease identify any issues."
b.ResetTimer()
for i := 0; i < b.N; i++ {
sanitizePrompt(prompt)
}
}
// BenchmarkSanitizePromptWithControlChars benchmarks sanitization with control characters.
func BenchmarkSanitizePromptWithControlChars(b *testing.B) {
prompt := "Hello\x00World\x01Test\x02Data\x03End"
b.ResetTimer()
for i := 0; i < b.N; i++ {
sanitizePrompt(prompt)
}
}
// TestSafeResolvePath tests the path traversal protection.
func TestSafeResolvePath(t *testing.T) {
// Create a temporary directory for testing
tmpDir := t.TempDir()
tests := []struct {
name string
path string
cwd string
wantPath string
wantOk bool
}{
{
name: "simple relative path",
path: "file.txt",
cwd: tmpDir,
wantOk: true,
wantPath: filepath.Join(tmpDir, "file.txt"),
},
{
name: "nested relative path",
path: "subdir/file.txt",
cwd: tmpDir,
wantOk: true,
wantPath: filepath.Join(tmpDir, "subdir", "file.txt"),
},
{
name: "path traversal with ..",
path: "../etc/passwd",
cwd: tmpDir,
wantOk: false,
},
{
name: "path traversal with multiple ..",
path: "../../etc/passwd",
cwd: tmpDir,
wantOk: false,
},
{
name: "path traversal hidden in middle",
path: "subdir/../../../etc/passwd",
cwd: tmpDir,
wantOk: false,
},
{
name: "just parent directory",
path: "..",
cwd: tmpDir,
wantOk: false,
},
{
name: "absolute path without cwd",
path: "/some/absolute/path",
cwd: "",
wantOk: true,
wantPath: "/some/absolute/path",
},
{
name: "relative path without cwd",
path: "relative/path",
cwd: "",
wantOk: true,
wantPath: "relative/path",
},
{
name: "current directory reference",
path: "./file.txt",
cwd: tmpDir,
wantOk: true,
wantPath: filepath.Join(tmpDir, "file.txt"),
},
{
name: "absolute path outside cwd",
path: "/etc/passwd",
cwd: tmpDir,
wantOk: false,
},
{
name: "absolute path inside cwd",
path: filepath.Join(tmpDir, "inside.txt"),
cwd: tmpDir,
wantOk: true,
wantPath: filepath.Join(tmpDir, "inside.txt"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPath, gotOk := safeResolvePath(tt.path, tt.cwd)
assert.Equal(t, tt.wantOk, gotOk, "ok status mismatch")
if tt.wantPath != "" && gotOk {
assert.Equal(t, tt.wantPath, gotPath, "path mismatch")
}
})
}
}