- 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
75 lines
2.4 KiB
Go
75 lines
2.4 KiB
Go
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)
|
|
}
|
|
}
|