package server import ( "net/http" "github.com/getsentry/sentry-go" ) // sentryRedacted stands in for a withheld field on every event shipped // to Sentry. It is a marker rather than an empty string so a reader // can tell a suppressed value from an absent one. const sentryRedacted = "(redacted)" // sentryClientOptions builds the options the SDK is initialised with. // It is its own function so a test can stand up a client wired exactly // as production is, with only the transport swapped. func sentryClientOptions(dsn, release string) sentry.ClientOptions { return sentry.ClientOptions{ Dsn: dsn, Release: release, // Both hooks, because the SDK runs one for error events // and the other for transactions. BeforeSend: scrubSentryRequest, BeforeSendTransaction: scrubSentryRequest, } } // scrubSentryRequest strips client-supplied content from an event's // request context before it leaves the process. // // sentryhttp attaches the whole *http.Request to the scope // (sentryhttp.go:113), and Scope.ApplyToEvent fills the event's // Request from it inside prepareEvent, which runs before this hook. // Two of the fields it fills are copied with no SendDefaultPII guard: // // - QueryString, verbatim from r.URL.RawQuery. // - Data, the first 10 KiB of the request body, teed off r.Body by // SetRequest and filled precisely because the handlers call // ParseForm. // // Since every form field in this service is read with PostFormValue, // the body is the only place a credential is submitted: a target's // destination URL, whose path segments are the bearer token, plus the // login password and both password-change fields. None of that may // reach a third-party service. // // This hook is a floor, not a default: the fields it clears stay // cleared even if SendDefaultPII is ever turned on. func scrubSentryRequest( event *sentry.Event, _ *sentry.EventHint, ) *sentry.Event { if event == nil || event.Request == nil { return event } req := event.Request if req.QueryString != "" { req.QueryString = sentryRedacted } if req.Data != "" { req.Data = sentryRedacted } req.Cookies = "" req.Env = nil req.Headers = keptSentryHeaders(req.Headers) return event } // keptSentryHeaders returns the subset of headers an event may carry // off-host. Dropping by allowlist rather than by blocklist is what // makes an unrecognised header safe: the SDK's own filter removes four // names and passes everything else, so X-Csrf-Token — which // gorilla/csrf accepts in place of the form field — and the shared // secrets senders put on the receiver route (X-Gitlab-Token and the // per-provider signature headers) would otherwise ship verbatim. func keptSentryHeaders(headers map[string]string) map[string]string { if len(headers) == 0 { return headers } kept := make(map[string]string, len(headers)) for name, value := range headers { if sentryKeepsHeader(name) { kept[name] = value } } return kept } // sentryKeepsHeader reports whether a request header is routing or // content metadata rather than client-chosen payload. Referer is kept // on the reasoning that it is browser-set, that this service emits // only ?page= in its own links, and that Referrer-Policy is set to // strict-origin-when-cross-origin. X-Request-Id ties the event to the // local access log line, which holds the rest of the detail. func sentryKeepsHeader(name string) bool { switch http.CanonicalHeaderKey(name) { case "Accept", "Content-Length", "Content-Type", "Host", "Origin", "Referer", "User-Agent", "X-Request-Id": return true default: return false } }