fix(14.2): WR-01 access log never records the URL path
An unmatched request logged its path. A token route reached with another method (405) or with a trailing slash (404) is answered by ServeMux before any route handler records the pattern, so the share or invitation token in the path reached the log. The record now carries method, pattern, status and duration only.
This commit is contained in:
@@ -109,11 +109,14 @@ func TestAccessLogSmoke(t *testing.T) {
|
||||
t.Fatalf("records = %d, want 1:\n%s", len(recs), buf.String())
|
||||
}
|
||||
r = recs[0]
|
||||
if r["pattern"] != "" || r["status"] != float64(404) || r["path"] != "/acme/missing" {
|
||||
if r["pattern"] != "" || r["status"] != float64(404) {
|
||||
t.Fatalf("unmatched record = %v", r)
|
||||
}
|
||||
if strings.Contains(buf.String(), "secret") {
|
||||
t.Fatalf("unmatched record leaks the query:\n%s", buf.String())
|
||||
if _, ok := r["path"]; ok {
|
||||
t.Fatalf("unmatched record carries path: %v", r)
|
||||
}
|
||||
if strings.Contains(buf.String(), "secret") || strings.Contains(buf.String(), "/acme/missing") {
|
||||
t.Fatalf("unmatched record leaks the path or query:\n%s", buf.String())
|
||||
}
|
||||
|
||||
off, offBuf := accessLogRouter(t, false)
|
||||
@@ -235,8 +238,9 @@ func accessRawRecords(t *testing.T, buf *bytes.Buffer) []map[string]any {
|
||||
|
||||
// TestAccessLog: off, nothing is logged. On, one record per request with
|
||||
// the matched pattern and status for a 200, a handler 404 and a recovered
|
||||
// panic (500); an unmatched request gets status 404 and its path without
|
||||
// the query; a locale-prefixed request still records the route pattern;
|
||||
// panic (500); an unmatched request (a 404, a 405 for a route reached with
|
||||
// another method, a trailing-slash 404) gets an empty pattern and no path;
|
||||
// a locale-prefixed request still records the route pattern;
|
||||
// duration_ms is a non-negative integer; no record carries the query
|
||||
// string, a header name or value or the Authorization value; and a
|
||||
// handler's http.ResponseController flush keeps working.
|
||||
@@ -244,21 +248,29 @@ func TestAccessLog(t *testing.T) {
|
||||
type want struct {
|
||||
pattern string
|
||||
status int64
|
||||
path string // "" means the record must have no path
|
||||
}
|
||||
cases := []struct {
|
||||
target string
|
||||
code int
|
||||
want want
|
||||
}{
|
||||
{"/blog/posts/7?token=query-secret", http.StatusOK, want{"GET /blog/posts/{id}", 200, ""}},
|
||||
{"/blog/drafts/9?token=query-secret", http.StatusNotFound, want{"GET /blog/drafts/{id}", 404, ""}},
|
||||
{"/blog/broken?token=query-secret", http.StatusInternalServerError, want{"GET /blog/broken", 500, ""}},
|
||||
{"/acme/missing?token=query-secret", http.StatusNotFound, want{"", 404, "/acme/missing"}},
|
||||
{"/pl/blog/posts/8?token=query-secret", http.StatusOK, want{"GET /blog/posts/{id}", 200, ""}},
|
||||
{"/blog/posts/7?token=query-secret", http.StatusOK, want{"GET /blog/posts/{id}", 200}},
|
||||
{"/blog/drafts/9?token=query-secret", http.StatusNotFound, want{"GET /blog/drafts/{id}", 404}},
|
||||
{"/blog/broken?token=query-secret", http.StatusInternalServerError, want{"GET /blog/broken", 500}},
|
||||
{"/acme/missing?token=query-secret", http.StatusNotFound, want{"", 404}},
|
||||
{"/pl/blog/posts/8?token=query-secret", http.StatusOK, want{"GET /blog/posts/{id}", 200}},
|
||||
// A token route reached with another method (405) or a trailing
|
||||
// slash (404) matches no route: ServeMux answers before any route
|
||||
// handler runs, so the path segment must not reach the log.
|
||||
{"POST /blog/posts/path-secret-405", http.StatusMethodNotAllowed, want{"", 405}},
|
||||
{"/blog/posts/path-secret-404/", http.StatusNotFound, want{"", 404}},
|
||||
}
|
||||
request := func(target string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, target, nil)
|
||||
method := http.MethodGet
|
||||
if m, rest, ok := strings.Cut(target, " "); ok {
|
||||
method, target = m, rest
|
||||
}
|
||||
req := httptest.NewRequest(method, target, nil)
|
||||
req.Header.Set("Authorization", "Bearer authz-secret-value")
|
||||
req.Header.Set("X-Blog-Probe", "probe-header-value")
|
||||
req.Header.Set("Cookie", "session=cookie-secret-value")
|
||||
@@ -279,7 +291,8 @@ func TestAccessLog(t *testing.T) {
|
||||
buf.Reset()
|
||||
flushErr = context.Canceled
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, request(tc.target))
|
||||
req := request(tc.target)
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != tc.code {
|
||||
t.Fatalf("GET %s = %d, want %d", tc.target, rec.Code, tc.code)
|
||||
}
|
||||
@@ -292,7 +305,7 @@ func TestAccessLog(t *testing.T) {
|
||||
}
|
||||
r := recs[0]
|
||||
status, err := r["status"].(json.Number).Int64()
|
||||
if err != nil || r["method"] != "GET" || r["pattern"] != tc.want.pattern || status != tc.want.status || r["level"] != "INFO" {
|
||||
if err != nil || r["method"] != req.Method || r["pattern"] != tc.want.pattern || status != tc.want.status || r["level"] != "INFO" {
|
||||
t.Fatalf("GET %s: record %v, want pattern %q status %d", tc.target, r, tc.want.pattern, tc.want.status)
|
||||
}
|
||||
d, ok := r["duration_ms"].(json.Number)
|
||||
@@ -302,17 +315,13 @@ func TestAccessLog(t *testing.T) {
|
||||
if n, err := strconv.ParseInt(d.String(), 10, 64); err != nil || n < 0 {
|
||||
t.Fatalf("GET %s: duration_ms %q is not a non-negative integer", tc.target, d)
|
||||
}
|
||||
path, hasPath := r["path"]
|
||||
if tc.want.path == "" && hasPath {
|
||||
t.Fatalf("GET %s: a matched record carries path: %v", tc.target, r)
|
||||
}
|
||||
if tc.want.path != "" && path != tc.want.path {
|
||||
t.Fatalf("GET %s: path = %v, want %q", tc.target, path, tc.want.path)
|
||||
if _, hasPath := r["path"]; hasPath {
|
||||
t.Fatalf("GET %s: the record carries path: %v", tc.target, r)
|
||||
}
|
||||
for _, leak := range []string{
|
||||
"query-secret", "token=", "authz-secret-value", "Authorization", "Bearer",
|
||||
"X-Blog-Probe", "probe-header-value", "cookie-secret-value", "Cookie",
|
||||
"/blog/posts/7", "/blog/posts/8", "/pl/",
|
||||
"/blog/posts/7", "/blog/posts/8", "/pl/", "path-secret", "/acme/missing",
|
||||
} {
|
||||
if strings.Contains(buf.String(), leak) {
|
||||
t.Fatalf("GET %s: the log carries %q:\n%s", tc.target, leak, buf.String())
|
||||
|
||||
Reference in New Issue
Block a user