All checks were successful
check / check (push) Successful in 9s
Go embeds the request URL in *url.Error, so any transport failure — DNS, TLS, refused, timeout, SSRF dial block — persisted the full Slack webhook URL into the per-webhook SQLite database via DeliveryResult.Error. That field is tagged json:"error,omitempty", so a future REST API would have served it. maskURLError rebuilds the error preserving Op and the wrapped cause, so DNS vs TLS vs timeout still read differently and errors.Is/As and Timeout() keep working; only path, query and userinfo are dropped. Applied where the errors are born, which covers both the Slack and HTTP targets. url.Parse embeds the URL too, so ValidateTargetURL's parse branch gets the same treatment. The SSRF rejection log now logs the masked URL, and source_logs.html receives view types rather than raw rows, so no config blob is reachable from that template. MaskURL is now the single masker for the whole tree.
62 lines
1.8 KiB
Go
62 lines
1.8 KiB
Go
package delivery
|
|
|
|
import (
|
|
"errors"
|
|
"net/url"
|
|
)
|
|
|
|
// urlPathElision stands in for a URL's elided path.
|
|
const urlPathElision = "/..."
|
|
|
|
// MaskURL renders a URL as scheme plus host with everything
|
|
// that can carry a secret removed. A delivery target URL is
|
|
// itself a credential — a Slack incoming webhook URL is a
|
|
// bearer token — so the path, query and userinfo are never
|
|
// reproduced, in a page, a log line or a stored error. A URL
|
|
// that does not parse into a scheme and host yields the
|
|
// neutral placeholder, never the raw string.
|
|
func MaskURL(raw string) string {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil || parsed.Scheme == "" ||
|
|
parsed.Host == "" {
|
|
return configUnavailable
|
|
}
|
|
|
|
masked := parsed.Scheme + "://" + parsed.Host
|
|
|
|
if parsed.Path != "" && parsed.Path != "/" {
|
|
masked += urlPathElision
|
|
}
|
|
|
|
return masked
|
|
}
|
|
|
|
// maskURLError strips the credential from an error raised
|
|
// against a request URL. The net/http and net/url packages
|
|
// embed the full request URL in every *url.Error they return,
|
|
// so an unmodified transport error persisted into
|
|
// DeliveryResult.Error writes the credential to disk.
|
|
//
|
|
// The masked error keeps the operation and the wrapped cause,
|
|
// so a DNS failure still reads differently from a refused
|
|
// connection, a TLS handshake failure or a timeout, and Is,
|
|
// As, Timeout and Temporary keep working on it. Only the
|
|
// path, query and userinfo of the URL are dropped. Errors
|
|
// that carry no URL are returned unchanged.
|
|
//
|
|
// Call it where the error is raised, before any wrapping: it
|
|
// replaces the *url.Error itself, so any context wrapped
|
|
// around it first would be discarded.
|
|
func maskURLError(err error) error {
|
|
var urlErr *url.Error
|
|
if !errors.As(err, &urlErr) {
|
|
return err
|
|
}
|
|
|
|
return &url.Error{
|
|
Op: urlErr.Op,
|
|
URL: MaskURL(urlErr.URL),
|
|
Err: urlErr.Err,
|
|
}
|
|
}
|