mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-02 01:38:17 +00:00
Redact invite code from analytics
This commit is contained in:
@@ -115,7 +115,7 @@ func (rec *Recorder) record(r *http.Request, status int) {
|
||||
Ts: time.Now(),
|
||||
Surface: rec.surface(host),
|
||||
Host: host,
|
||||
Path: r.URL.Path,
|
||||
Path: redactPath(r.URL.Path),
|
||||
Method: r.Method,
|
||||
Status: status,
|
||||
Visitor: rec.visitor(r),
|
||||
@@ -126,6 +126,25 @@ func (rec *Recorder) record(r *http.Request, status int) {
|
||||
}
|
||||
}
|
||||
|
||||
// redactPath replaces a secret path segment with a placeholder before an event
|
||||
// is recorded, so a share-link/invite token — a live secret until the invite is
|
||||
// accepted — doesn't sit in the raw events table for the retention window. The
|
||||
// invite token is the only secret carried in a URL *path*; the login-handoff
|
||||
// codes travel in the query string, which we never record. Templatizing (rather
|
||||
// than hashing) also keeps the aggregate meaningful: every invite hit rolls up
|
||||
// under one path.
|
||||
func redactPath(p string) string {
|
||||
rest, ok := strings.CutPrefix(p, "/invite/")
|
||||
if !ok || rest == "" {
|
||||
return p
|
||||
}
|
||||
// /invite/{token} → /invite/:token; /invite/{token}/accept keeps the tail.
|
||||
if i := strings.IndexByte(rest, '/'); i >= 0 {
|
||||
return "/invite/:token" + rest[i:]
|
||||
}
|
||||
return "/invite/:token"
|
||||
}
|
||||
|
||||
// surface classifies a request host into one of the known surfaces.
|
||||
func (rec *Recorder) surface(host string) string {
|
||||
switch {
|
||||
|
||||
@@ -30,6 +30,46 @@ func TestHandlerRecordsRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedactPath: the secret invite/share token is templatized so it never
|
||||
// reaches the raw events table; non-invite paths pass through untouched.
|
||||
// Regression for the pre-release audit finding that live tokens were recorded.
|
||||
func TestRedactPath(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"/invite/abc123secret", "/invite/:token"},
|
||||
{"/invite/abc123secret/accept", "/invite/:token/accept"},
|
||||
{"/invite/", "/invite/"}, // no token, nothing to redact
|
||||
{"/invite", "/invite"},
|
||||
{"/dashboard", "/dashboard"},
|
||||
{"/r/pub-id-not-secret", "/r/pub-id-not-secret"},
|
||||
{"/orgs/some-slug", "/orgs/some-slug"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := redactPath(c.in); got != c.want {
|
||||
t.Errorf("redactPath(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandlerRedactsInviteToken confirms the redaction happens on the real
|
||||
// recording path (the enqueued event), not just in the helper.
|
||||
func TestHandlerRedactsInviteToken(t *testing.T) {
|
||||
rec := testRecorder()
|
||||
h := rec.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://app.x/invite/S3cr3tShareToken", nil)
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
h.ServeHTTP(httptest.NewRecorder(), req)
|
||||
|
||||
select {
|
||||
case e := <-rec.ch:
|
||||
if e.Path != "/invite/:token" {
|
||||
t.Fatalf("recorded path = %q, want the token redacted to /invite/:token", e.Path)
|
||||
}
|
||||
default:
|
||||
t.Fatal("expected an event to be enqueued")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerSkips(t *testing.T) {
|
||||
rec := testRecorder()
|
||||
h := rec.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
|
||||
Reference in New Issue
Block a user