feat(14-01): sunscreen redacting slog handler installed by every generated main
- Wrap redacts sensitive keys at any depth and scrubs Bearer, sk- and x-api-key shapes - InstallDefault is the first statement of the generated run; hello main regenerated - surf test pins that recovered panics echo no credential - sunscreen README, root modules row and the logging docs page
This commit is contained in:
74
modules/surf/recover_redaction_test.go
Normal file
74
modules/surf/recover_redaction_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package surf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/sunscreen"
|
||||
)
|
||||
|
||||
// TestRecoverHidesPanicDetails pins the SafeExceptionResponse behaviour: a
|
||||
// panicking handler's message, here carrying credentials, never reaches the
|
||||
// response in either group type, and a log record of the panic written
|
||||
// through a sunscreen handler carries neither credential.
|
||||
func TestRecoverHidesPanicDetails(t *testing.T) {
|
||||
const skKey = "sk-abcdefghijklmnopqrstuvwxyz0123"
|
||||
const token = "eyJhbGciOiJIUzI1NiJ9.claims.signature"
|
||||
panicErr := errors.New("vendor rejected key " + skKey + " with Authorization: Bearer " + token)
|
||||
|
||||
prev := slog.Default()
|
||||
t.Cleanup(func() { slog.SetDefault(prev) })
|
||||
var logs bytes.Buffer
|
||||
sunscreen.InstallDefault(&logs)
|
||||
|
||||
r := New(nil)
|
||||
r.GroupRaw("/oauth", nil, func(g pact.Router) {
|
||||
g.Post("/token", func(http.ResponseWriter, *http.Request) { panic(panicErr) })
|
||||
})
|
||||
r.Post("/api/v1/recognize", func(http.ResponseWriter, *http.Request) { panic(panicErr) })
|
||||
h, err := r.compile()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
path, body, contentType string
|
||||
}{
|
||||
{"/api/v1/recognize", `{"error":true,"message":"Internal server error"}`, "application/json"},
|
||||
{"/oauth/token", "", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, c.path, nil))
|
||||
if rec.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("%s status = %d", c.path, rec.Code)
|
||||
}
|
||||
body := strings.TrimSpace(rec.Body.String())
|
||||
if body != c.body {
|
||||
t.Fatalf("%s body = %q, want the opaque %q", c.path, body, c.body)
|
||||
}
|
||||
if got := rec.Header().Get("Content-Type"); got != c.contentType {
|
||||
t.Fatalf("%s Content-Type = %q", c.path, got)
|
||||
}
|
||||
for _, secret := range []string{skKey, token, "vendor rejected"} {
|
||||
if strings.Contains(rec.Body.String(), secret) {
|
||||
t.Fatalf("%s response echoes %q", c.path, secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slog.Error("handler panicked", "err", panicErr, "path", "/api/v1/recognize")
|
||||
out := logs.String()
|
||||
if strings.Contains(out, skKey) || strings.Contains(out, token) {
|
||||
t.Fatalf("log record leaks a credential:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "sk-[REDACTED]") || !strings.Contains(out, "Bearer [REDACTED]") {
|
||||
t.Fatalf("log record not scrubbed as expected:\n%s", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user