108 lines
3.6 KiB
Go
108 lines
3.6 KiB
Go
package delivery
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// maxDeliveryRedirects caps a redirect chain. Installing a
|
|
// CheckRedirect replaces net/http's default policy including its
|
|
// own limit, so the limit is restated rather than dropped.
|
|
const maxDeliveryRedirects = 10
|
|
|
|
// schemeHTTPS names the scheme the origin comparison treats
|
|
// specially: a step down from it is never the same origin.
|
|
const schemeHTTPS = "https"
|
|
|
|
var errTooManyRedirects = errors.New("too many redirects")
|
|
|
|
// offOriginHeaderPolicy returns a CheckRedirect that drops every
|
|
// origin-scoped header once a redirect leaves the origin the
|
|
// operator configured. names is the set applyRequestHeaders
|
|
// reports: the operator's configured headers and the inbound event
|
|
// headers this delivery forwarded, under one rule rather than two.
|
|
//
|
|
// net/http withholds Authorization and Cookie across a host change
|
|
// and forwards everything else. A target header is routinely a
|
|
// credential under another name — X-Api-Key, PRIVATE-TOKEN,
|
|
// X-Auth-Token — and a forwarded inbound header is routinely a
|
|
// sender's signature — X-Hub-Signature — so an open redirect at an
|
|
// otherwise trusted destination would hand either to a host the
|
|
// operator never named. Redirects are still followed: refusing them
|
|
// would break every destination that legitimately redirects and
|
|
// would record the 3xx as the delivery's result.
|
|
//
|
|
// The strip is per hop, not permanent: net/http re-copies the
|
|
// initial request's headers for every hop, so a chain that returns
|
|
// to the configured origin carries them again, exactly as net/http
|
|
// treats Authorization.
|
|
//
|
|
// Each hop is dialled through the same SSRF-safe transport, whose
|
|
// guard runs per connection, so a redirect aimed at a private or
|
|
// reserved address is still refused at connect time.
|
|
func offOriginHeaderPolicy(
|
|
names []string,
|
|
) func(*http.Request, []*http.Request) error {
|
|
return func(req *http.Request, via []*http.Request) error {
|
|
if len(via) >= maxDeliveryRedirects {
|
|
return fmt.Errorf(
|
|
"%w: stopped after %d",
|
|
errTooManyRedirects, maxDeliveryRedirects,
|
|
)
|
|
}
|
|
|
|
if sameDeliveryOrigin(via[0].URL, req.URL) {
|
|
return nil
|
|
}
|
|
|
|
for _, name := range names {
|
|
req.Header.Del(name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// sameDeliveryOrigin reports whether dest is close enough to the
|
|
// configured target URL to keep carrying its origin-scoped headers.
|
|
//
|
|
// This is stricter than the rule net/http applies to Authorization:
|
|
// the port is part of the comparison (a different port is a
|
|
// different service), and a subdomain of the configured host is not
|
|
// the same origin. An https origin stepping down to http is never
|
|
// the same origin whatever the hosts are, because that puts the
|
|
// header on the wire in clear.
|
|
func sameDeliveryOrigin(origin, dest *url.URL) bool {
|
|
if origin.Scheme == schemeHTTPS && dest.Scheme != schemeHTTPS {
|
|
return false
|
|
}
|
|
|
|
return originHostPort(origin) == originHostPort(dest)
|
|
}
|
|
|
|
// originHostPort renders a URL's host for comparison, lowercased
|
|
// and with the scheme's default port normalised away so that
|
|
// "https://h" and "https://h:443" are one origin.
|
|
//
|
|
// The port is joined with net.JoinHostPort rather than a bare
|
|
// colon: Hostname() unwraps an IPv6 literal's brackets, so
|
|
// "[2001:db8::1]:8080" and "[2001:db8::1:8080]" — a different
|
|
// address on a different port — would otherwise render the same
|
|
// string and pass as one origin.
|
|
func originHostPort(u *url.URL) string {
|
|
host := strings.ToLower(u.Hostname())
|
|
|
|
port := u.Port()
|
|
if port == "" ||
|
|
(u.Scheme == "http" && port == "80") ||
|
|
(u.Scheme == schemeHTTPS && port == "443") {
|
|
return host
|
|
}
|
|
|
|
return net.JoinHostPort(host, port)
|
|
}
|