Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f2ead658 |
No files matched your search
@@ -2494,9 +2494,6 @@ are both taken unconditionally, the body precisely because these handlers call
|
|||||||
body with `(redacted)`, drops cookies and the remote-address environment, and
|
body with `(redacted)`, drops cookies and the remote-address environment, and
|
||||||
reduces the headers to a fixed allowlist — `Accept`, `Content-Length`,
|
reduces the headers to a fixed allowlist — `Accept`, `Content-Length`,
|
||||||
`Content-Type`, `Host`, `Origin`, `Referer`, `User-Agent` and `X-Request-Id`.
|
`Content-Type`, `Host`, `Origin`, `Referer`, `User-Agent` and `X-Request-Id`.
|
||||||
`Referer` is sent as its scheme and host only, because its path can be an
|
|
||||||
entrypoint URL; `internal/referer.SchemeAndHost` does that reduction for both
|
|
||||||
the access log's `referer` field and this hook.
|
|
||||||
|
|
||||||
The same hook rewrites the request URL. The SDK builds it as
|
The same hook rewrites the request URL. The SDK builds it as
|
||||||
`scheme://host/path` from the concrete path, which on the receiver route is
|
`scheme://host/path` from the concrete path, which on the receiver route is
|
||||||
@@ -2567,9 +2564,8 @@ receiver route. What survives still names the failing route — scheme, host,
|
|||||||
route pattern, method — and `X-Request-Id` ties the event to the local access
|
route pattern, method — and `X-Request-Id` ties the event to the local access
|
||||||
log line that holds the rest. Nothing dropped is needed for the likeliest use,
|
log line that holds the rest. Nothing dropped is needed for the likeliest use,
|
||||||
debugging a CSRF rejection. Its three inputs are the TLS decision, `Origin` and
|
debugging a CSRF rejection. Its three inputs are the TLS decision, `Origin` and
|
||||||
`Referer`; the latter two are kept (`Referer` as its scheme and host, which is
|
`Referer`; the latter two are kept, and the first is the scheme of the retained
|
||||||
all the check compares), and the first is the scheme of the retained URL,
|
URL, because the SDK derives that scheme from
|
||||||
because the SDK derives that scheme from
|
|
||||||
`r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"`. That predicate
|
`r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"`. That predicate
|
||||||
is the SDK's own and is stricter than `internal/reqtls.IsTLS`, which this
|
is the SDK's own and is stricter than `internal/reqtls.IsTLS`, which this
|
||||||
service now uses everywhere it decides transport: the SDK reports `http` for the
|
service now uses everywhere it decides transport: the SDK reports `http` for the
|
||||||
@@ -3101,8 +3097,6 @@ webhooker/
|
|||||||
│ │ ├── ratelimit.go # Per-IP rate limiting middleware (go-chi/httprate)
|
│ │ ├── ratelimit.go # Per-IP rate limiting middleware (go-chi/httprate)
|
||||||
│ │ ├── loginguard.go # Login failure counters and the Argon2id verification semaphore
|
│ │ ├── loginguard.go # Login failure counters and the Argon2id verification semaphore
|
||||||
│ │ └── middlewaretest/ # Test support: a Middleware for tests in other packages
|
│ │ └── middlewaretest/ # Test support: a Middleware for tests in other packages
|
||||||
│ ├── referer/
|
|
||||||
│ │ └── referer.go # SchemeAndHost: cuts a Referer down to its scheme and host, for the access log and the Sentry hook
|
|
||||||
│ ├── reqtls/
|
│ ├── reqtls/
|
||||||
│ │ └── reqtls.go # IsTLS: the one TLS predicate, r.TLS or X-Forwarded-Proto
|
│ │ └── reqtls.go # IsTLS: the one TLS predicate, r.TLS or X-Forwarded-Proto
|
||||||
│ ├── server/
|
│ ├── server/
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/config"
|
"sneak.berlin/go/webhooker/internal/config"
|
||||||
"sneak.berlin/go/webhooker/internal/globals"
|
"sneak.berlin/go/webhooker/internal/globals"
|
||||||
"sneak.berlin/go/webhooker/internal/logfield"
|
"sneak.berlin/go/webhooker/internal/logfield"
|
||||||
"sneak.berlin/go/webhooker/internal/referer"
|
|
||||||
"sneak.berlin/go/webhooker/internal/session"
|
"sneak.berlin/go/webhooker/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,6 +48,10 @@ const (
|
|||||||
// pick the size of the line it writes.
|
// pick the size of the line it writes.
|
||||||
redactedQuery = "?(redacted)"
|
redactedQuery = "?(redacted)"
|
||||||
|
|
||||||
|
// redactedReferer stands in for a Referer the access log cannot
|
||||||
|
// reduce to its scheme and host; see accessLogReferer.
|
||||||
|
redactedReferer = "(redacted)"
|
||||||
|
|
||||||
// maxLogRequestIDBytes bounds the request id, which is also
|
// maxLogRequestIDBytes bounds the request id, which is also
|
||||||
// client-supplied: chi's RequestID middleware passes an inbound
|
// client-supplied: chi's RequestID middleware passes an inbound
|
||||||
// X-Request-Id header through verbatim. Its generated form is an
|
// X-Request-Id header through verbatim. Its generated form is an
|
||||||
@@ -115,15 +118,16 @@ const (
|
|||||||
// a caller on a parameterised route would supply, which is the
|
// a caller on a parameterised route would supply, which is the
|
||||||
// only way those caps can be pinned at all.
|
// only way those caps can be pinned at all.
|
||||||
//
|
//
|
||||||
// The GORM adapter in internal/gormlog writes through slog as well.
|
// The GORM adapter in internal/gormlog writes through slog as well,
|
||||||
// It logs each statement with its placeholders rather than the
|
// though not from a handler. It logs each statement with its
|
||||||
// values bound to them (see its ParamsFilter), so the key a client
|
// placeholders rather than the values bound to them (see its
|
||||||
// sends to the receiver or login lookup does not reach its lines.
|
// ParamsFilter), so the key a client sends to the receiver or login
|
||||||
// Its widest line spends two logfield.MaxBytes budgets — the
|
// lookup does not reach its lines. Its widest line spends two
|
||||||
// statement and the driver error — against a fixed portion smaller
|
// logfield.MaxBytes budgets — the statement and the driver error —
|
||||||
// than this one's, and internal/gormlog/gormlog_test.go asserts
|
// against a fixed portion smaller than this one's, and
|
||||||
// every line it emits against this constant directly rather than
|
// internal/gormlog/gormlog_test.go asserts every line it emits
|
||||||
// leaving it as arithmetic.
|
// against this constant directly rather than leaving it as
|
||||||
|
// arithmetic.
|
||||||
//
|
//
|
||||||
// What it does NOT cover, so that the figure above is not read as
|
// What it does NOT cover, so that the figure above is not read as
|
||||||
// more than it is:
|
// more than it is:
|
||||||
@@ -338,6 +342,27 @@ func accessLogURL(r *http.Request, status int) string {
|
|||||||
return pattern
|
return pattern
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// accessLogReferer returns the value for the access log's referer
|
||||||
|
// field: the Referer's scheme and host, never its path or query. The
|
||||||
|
// path can be an entrypoint URL, which carries the UUID: a sender that
|
||||||
|
// follows a redirect from http:// to https:// sends the URL it was
|
||||||
|
// redirected from, and a browser sends the 404 page's URL under /h/
|
||||||
|
// when it fetches that page's stylesheet. A Referer without both a
|
||||||
|
// scheme and a host is logged as redactedReferer.
|
||||||
|
func accessLogReferer(r *http.Request) string {
|
||||||
|
referer := r.Referer()
|
||||||
|
if referer == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := url.Parse(referer)
|
||||||
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
|
return redactedReferer
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.Scheme + "://" + parsed.Host
|
||||||
|
}
|
||||||
|
|
||||||
// Logging returns middleware that logs each HTTP request with
|
// Logging returns middleware that logs each HTTP request with
|
||||||
// timing and metadata.
|
// timing and metadata.
|
||||||
func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
||||||
@@ -388,8 +413,7 @@ func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
|||||||
requestID, maxLogRequestIDBytes,
|
requestID, maxLogRequestIDBytes,
|
||||||
),
|
),
|
||||||
"referer", logfield.Truncate(
|
"referer", logfield.Truncate(
|
||||||
referer.SchemeAndHost(r.Referer()),
|
accessLogReferer(r), logfield.MaxBytes,
|
||||||
logfield.MaxBytes,
|
|
||||||
),
|
),
|
||||||
"proto", r.Proto,
|
"proto", r.Proto,
|
||||||
"remoteIP", RemoteIP(r),
|
"remoteIP", RemoteIP(r),
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
// Package referer reduces a Referer header to its scheme and host, so
|
|
||||||
// that a URL path, which on the receiver route carries the entrypoint
|
|
||||||
// UUID, never reaches a log line or an error report.
|
|
||||||
package referer
|
|
||||||
|
|
||||||
import "net/url"
|
|
||||||
|
|
||||||
// redacted stands in for a Referer that cannot be reduced to its
|
|
||||||
// scheme and host.
|
|
||||||
const redacted = "(redacted)"
|
|
||||||
|
|
||||||
// SchemeAndHost returns a Referer's scheme and host, never its path or
|
|
||||||
// query. The path can be an entrypoint URL, which carries the UUID: a
|
|
||||||
// sender that follows a redirect from http:// to https:// sends the
|
|
||||||
// URL it was redirected from, and a browser sends the 404 page's URL
|
|
||||||
// under /h/ when it fetches that page's stylesheet. A Referer without
|
|
||||||
// both a scheme and a host comes back as "(redacted)".
|
|
||||||
//
|
|
||||||
// It is the access log's referer field, and the Referer header the
|
|
||||||
// Sentry hook sends.
|
|
||||||
func SchemeAndHost(referer string) string {
|
|
||||||
if referer == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := url.Parse(referer)
|
|
||||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
||||||
return redacted
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed.Scheme + "://" + parsed.Host
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package referer_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"sneak.berlin/go/webhooker/internal/referer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// entrypointUUID is the secret part of an entrypoint URL; no case may
|
|
||||||
// return it.
|
|
||||||
const entrypointUUID = "0b6f7d3e-6a39-4e3c-9f0a-2c4d8e1b5a77"
|
|
||||||
|
|
||||||
// redacted is what SchemeAndHost returns for a Referer it cannot
|
|
||||||
// reduce to a scheme and host.
|
|
||||||
const redacted = "(redacted)"
|
|
||||||
|
|
||||||
func TestSchemeAndHost(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
referer string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no Referer",
|
|
||||||
referer: "",
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "entrypoint URL with a query",
|
|
||||||
referer: "http://webhooker.example/h/" + entrypointUUID +
|
|
||||||
"?src=ci",
|
|
||||||
want: "http://webhooker.example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "host with a port",
|
|
||||||
referer: "https://webhooker.example:8443/h/" + entrypointUUID,
|
|
||||||
want: "https://webhooker.example:8443",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path without a scheme or host",
|
|
||||||
referer: "/h/" + entrypointUUID,
|
|
||||||
want: redacted,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "host without a scheme",
|
|
||||||
referer: "webhooker.example/h/" + entrypointUUID,
|
|
||||||
want: redacted,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "not a URL",
|
|
||||||
referer: "http://[::1/h/" + entrypointUUID,
|
|
||||||
want: redacted,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
got := referer.SchemeAndHost(tc.referer)
|
|
||||||
|
|
||||||
assert.Equal(t, tc.want, got)
|
|
||||||
assert.NotContains(t, got, entrypointUUID)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"sneak.berlin/go/webhooker/internal/referer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// sentryRedacted stands in for a withheld field on every event shipped
|
// sentryRedacted stands in for a withheld field on every event shipped
|
||||||
@@ -254,7 +253,6 @@ func sentryTransactionName(name, pattern string) string {
|
|||||||
// gorilla/csrf accepts in place of the form field — and the shared
|
// gorilla/csrf accepts in place of the form field — and the shared
|
||||||
// secrets senders put on the receiver route (X-Gitlab-Token and the
|
// secrets senders put on the receiver route (X-Gitlab-Token and the
|
||||||
// per-provider signature headers) would otherwise ship verbatim.
|
// per-provider signature headers) would otherwise ship verbatim.
|
||||||
// The Referer it keeps is cut down to its scheme and host.
|
|
||||||
func keptSentryHeaders(headers map[string]string) map[string]string {
|
func keptSentryHeaders(headers map[string]string) map[string]string {
|
||||||
if len(headers) == 0 {
|
if len(headers) == 0 {
|
||||||
return headers
|
return headers
|
||||||
@@ -263,15 +261,9 @@ func keptSentryHeaders(headers map[string]string) map[string]string {
|
|||||||
kept := make(map[string]string, len(headers))
|
kept := make(map[string]string, len(headers))
|
||||||
|
|
||||||
for name, value := range headers {
|
for name, value := range headers {
|
||||||
if !sentryKeepsHeader(name) {
|
if sentryKeepsHeader(name) {
|
||||||
continue
|
kept[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
if http.CanonicalHeaderKey(name) == "Referer" {
|
|
||||||
value = referer.SchemeAndHost(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
kept[name] = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return kept
|
return kept
|
||||||
@@ -279,14 +271,11 @@ func keptSentryHeaders(headers map[string]string) map[string]string {
|
|||||||
|
|
||||||
// sentryKeepsHeader reports whether a request header is routing or
|
// sentryKeepsHeader reports whether a request header is routing or
|
||||||
// content metadata rather than client-chosen payload. Referer is kept
|
// content metadata rather than client-chosen payload. Referer is kept
|
||||||
// only as its scheme and host, through referer.SchemeAndHost, which
|
// on the reasoning that it is browser-set, that the only query
|
||||||
// the access log uses too: its path can be an entrypoint URL, which
|
// parameters in this service's own URLs are the login page's `next`,
|
||||||
// carries the UUID, as when a sender follows a redirect from http://
|
// `notice` and the event log's `show`, and that Referrer-Policy is set to
|
||||||
// to https:// or a browser fetches the 404 page's stylesheet from
|
// strict-origin-when-cross-origin. X-Request-Id ties the event to the
|
||||||
// under /h/. The scheme and host are all gorilla/csrf compares a
|
// local access log line, which holds the rest of the detail.
|
||||||
// Referer by, so a CSRF rejection is still readable from the event.
|
|
||||||
// X-Request-Id ties the event to the local access log line, which
|
|
||||||
// holds the rest of the detail.
|
|
||||||
func sentryKeepsHeader(name string) bool {
|
func sentryKeepsHeader(name string) bool {
|
||||||
switch http.CanonicalHeaderKey(name) {
|
switch http.CanonicalHeaderKey(name) {
|
||||||
case "Accept",
|
case "Accept",
|
||||||
|
|||||||
@@ -325,37 +325,6 @@ func TestSentryScrub_ReplacesTheCapabilityPathWithTheRoutePattern(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSentryScrub_KeepsOnlyTheRefererSchemeAndHost sends a receiver
|
|
||||||
// request whose Referer is the entrypoint URL, as a sender's is after
|
|
||||||
// it follows the README's redirect from http:// to https://. The event
|
|
||||||
// keeps the Referer's scheme and host, and carries the UUID nowhere.
|
|
||||||
func TestSentryScrub_KeepsOnlyTheRefererSchemeAndHost(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
event := onlyEvent(t, sentryCase{
|
|
||||||
scrub: true,
|
|
||||||
panics: true,
|
|
||||||
request: func(client *sentry.Client) *http.Request {
|
|
||||||
req := sentryReceiverRequest(client)
|
|
||||||
req.Header.Set(
|
|
||||||
"Referer",
|
|
||||||
"http://webhooker.example/h/"+sentryReceiverUUID,
|
|
||||||
)
|
|
||||||
|
|
||||||
return req
|
|
||||||
},
|
|
||||||
}.capture(t))
|
|
||||||
|
|
||||||
assert.NotContains(
|
|
||||||
t, marshalEvent(t, event), sentryReceiverUUID,
|
|
||||||
)
|
|
||||||
assert.Equal(
|
|
||||||
t,
|
|
||||||
"http://webhooker.example",
|
|
||||||
event.Request.Headers["Referer"],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSentryScrub_KeepsTheRoutingContext checks the hook does not cost
|
// TestSentryScrub_KeepsTheRoutingContext checks the hook does not cost
|
||||||
// the debugging signal: the route, its scheme and host, the method and
|
// the debugging signal: the route, its scheme and host, the method and
|
||||||
// the metadata headers still identify what failed. On a static route
|
// the metadata headers still identify what failed. On a static route
|
||||||
|
|||||||
Reference in new issue
Block a user