Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa67d883e9 |
@@ -157,19 +157,6 @@ private and reserved ranges — RFC 1918, loopback, CGNAT, link-local and
|
|||||||
the rest — are refused, which stops a target from being used to make
|
the rest — are refused, which stops a target from being used to make
|
||||||
webhooker probe the network it sits in.
|
webhooker probe the network it sits in.
|
||||||
|
|
||||||
Besides the private and reserved ranges, the default blocklist refuses
|
|
||||||
public cloud metadata addresses: currently only `168.63.129.16`, Azure's
|
|
||||||
WireServer, which serves an Azure VM its credentials. Because it is a
|
|
||||||
public address, listing it in `ALLOWED_EGRESS_CIDRS` reopens it.
|
|
||||||
|
|
||||||
That is all the default blocklist covers: private and reserved space,
|
|
||||||
plus public addresses that serve cloud credentials. A cloud provider's
|
|
||||||
other services on public addresses are not refused — IBM Cloud's
|
|
||||||
`161.26.0.0/16` and `166.8.0.0/14`, for example, which carry its DNS
|
|
||||||
resolvers, time servers and package mirrors. They serve no credentials,
|
|
||||||
reaching them can be a legitimate delivery, and every cloud has some, so
|
|
||||||
a partial list would promise coverage it does not give.
|
|
||||||
|
|
||||||
That default is also inconvenient for the thing webhooker is mostly
|
That default is also inconvenient for the thing webhooker is mostly
|
||||||
for: taking a public webhook and forwarding it to something on your own
|
for: taking a public webhook and forwarding it to something on your own
|
||||||
network. A container on the same Docker network, a box on `10.x`, a
|
network. A container on the same Docker network, a box on `10.x`, a
|
||||||
@@ -208,16 +195,15 @@ Two things this setting cannot do:
|
|||||||
the list is always an allowlist; an empty list (the default) means
|
the list is always an allowlist; an empty list (the default) means
|
||||||
every private and reserved range stays refused. Note that
|
every private and reserved range stays refused. Note that
|
||||||
`0.0.0.0/0` gets you most of the way there anyway, per above.
|
`0.0.0.0/0` gets you most of the way there anyway, per above.
|
||||||
- **It cannot open link-local, or a cloud metadata endpoint at a
|
- **It cannot open link-local, or a cloud metadata endpoint that
|
||||||
non-public address that discloses credentials or user data.** An
|
discloses credentials or user data.** An address is on the list below
|
||||||
address is on the list below when it is not a public address and both
|
when both of these hold: the provider fixes it, so it cannot collide
|
||||||
of these hold: the provider fixes it, so it cannot collide with
|
with anything you run; and reaching it hands out credentials, user
|
||||||
anything you run; and reaching it hands out credentials, user data or
|
data or bootstrap material. Those stay blocked no matter what you
|
||||||
bootstrap material. Those stay blocked no matter what you list,
|
list, including when you list them outright or list a supernet such
|
||||||
including when you list them outright or list a supernet such as
|
as `0.0.0.0/0`, `::/0`, `fd00::/8` or `100.64.0.0/10`. Treat this as
|
||||||
`0.0.0.0/0`, `::/0`, `fd00::/8` or `100.64.0.0/10`. Treat this as best
|
best effort rather than a guarantee — it is a hand-maintained list
|
||||||
effort rather than a guarantee — it is a hand-maintained list and the
|
and the caveat below the table applies:
|
||||||
caveat below the table applies:
|
|
||||||
|
|
||||||
| Blocked unconditionally | What it is |
|
| Blocked unconditionally | What it is |
|
||||||
| ----------------------- | ---------- |
|
| ----------------------- | ---------- |
|
||||||
@@ -256,8 +242,7 @@ Two things this setting cannot do:
|
|||||||
encodings, which the default blocklist does not match. A publicly
|
encodings, which the default blocklist does not match. A publicly
|
||||||
routable metadata address is not listed here, because nothing on this
|
routable metadata address is not listed here, because nothing on this
|
||||||
list can be reopened and blocking one that way would leave you no
|
list can be reopened and blocking one that way would leave you no
|
||||||
escape hatch at all; Azure's `168.63.129.16` is refused by the default
|
escape hatch at all.
|
||||||
blocklist instead, as described above.
|
|
||||||
|
|
||||||
This list is not exhaustive of every cloud's metadata address — if
|
This list is not exhaustive of every cloud's metadata address — if
|
||||||
yours is not here, do not allowlist the block that contains it.
|
yours is not here, do not allowlist the block that contains it.
|
||||||
|
|||||||
@@ -192,10 +192,9 @@ type Config struct {
|
|||||||
// alwaysBlockedNetworks stays blocked no matter what is listed
|
// alwaysBlockedNetworks stays blocked no matter what is listed
|
||||||
// here. That set is link-local plus the cloud metadata
|
// here. That set is link-local plus the cloud metadata
|
||||||
// endpoints outside it that disclose credentials or user data
|
// endpoints outside it that disclose credentials or user data
|
||||||
// at a provider-fixed, non-public address; it is not
|
// at a provider-fixed address; it is not exhaustive of every
|
||||||
// exhaustive of every cloud's metadata address. See
|
// cloud's metadata address. See alwaysBlockedNetworks for the
|
||||||
// alwaysBlockedNetworks for the authoritative list and the
|
// authoritative list and the criterion it is built from.
|
||||||
// criterion it is built from.
|
|
||||||
AllowedEgressCIDRs []netip.Prefix
|
AllowedEgressCIDRs []netip.Prefix
|
||||||
|
|
||||||
params *ConfigParams
|
params *ConfigParams
|
||||||
@@ -747,14 +746,12 @@ func (c *Config) warnEgressAllowlist(log *slog.Logger) {
|
|||||||
|
|
||||||
log.Warn(
|
log.Warn(
|
||||||
"ALLOWED_EGRESS_CIDRS lets delivery targets reach these "+
|
"ALLOWED_EGRESS_CIDRS lets delivery targets reach these "+
|
||||||
"otherwise-blocked networks. Anyone who can create a "+
|
"otherwise-blocked private/reserved networks. Anyone "+
|
||||||
"delivery target can now make this process issue "+
|
"who can create a delivery target can now make this "+
|
||||||
"requests into them, and read back the response. Only "+
|
"process issue requests into them, and read back the "+
|
||||||
"the addresses the README lists as blocked "+
|
"response. Link-local and the known cloud instance "+
|
||||||
"unconditionally stay blocked regardless of what is "+
|
"metadata endpoints outside it stay blocked "+
|
||||||
"listed here; a public cloud metadata address such as "+
|
"regardless of what is listed here.",
|
||||||
"168.63.129.16 is reachable once it, or a block "+
|
|
||||||
"covering it, is listed.",
|
|
||||||
"allowedEgressCIDRs",
|
"allowedEgressCIDRs",
|
||||||
strings.Join(PrefixStrings(c.AllowedEgressCIDRs), ","),
|
strings.Join(PrefixStrings(c.AllowedEgressCIDRs), ","),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -834,13 +834,12 @@ func TestEgressAllowlistWarning(t *testing.T) {
|
|||||||
// to be able to read back which networks are open.
|
// to be able to read back which networks are open.
|
||||||
assert.Contains(t, logged, "10.0.0.0/8")
|
assert.Contains(t, logged, "10.0.0.0/8")
|
||||||
assert.Contains(t, logged, "127.0.0.0/8")
|
assert.Contains(t, logged, "127.0.0.0/8")
|
||||||
// What stays shut is the whole unconditional set, not
|
// What stays shut. Asserted on the clause naming the
|
||||||
// link-local alone; a public metadata address is not in
|
// wider set rather than on "Link-local" alone, so the
|
||||||
// it, so a listed block covering it opens it.
|
// string cannot narrow back to link-local only while
|
||||||
assert.Contains(t, logged, "blocked unconditionally")
|
// the always-blocked set covers ULA, CGNAT and two
|
||||||
assert.Contains(t, logged, "168.63.129.16 is reachable")
|
// public metadata addresses as well.
|
||||||
// The listed blocks need not be private or reserved.
|
assert.Contains(t, logged, "metadata endpoints outside it")
|
||||||
assert.NotContains(t, logged, "private/reserved")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,9 +368,9 @@ func (e *Engine) start() {
|
|||||||
// writer for long by then: the archive sweeper stops before the
|
// writer for long by then: the archive sweeper stops before the
|
||||||
// engine, and deleting a webhook only closes one. If the pool did
|
// engine, and deleting a webhook only closes one. If the pool did
|
||||||
// not drain in time, the writers are left open, as a kill would
|
// not drain in time, the writers are left open, as a kill would
|
||||||
// leave them. Closing them would wait for any write in progress,
|
// leave them: a worker still running may be mid-write, and
|
||||||
// and a worker still running would then open new writers that
|
// closing its writer would wait on that write and then fail the
|
||||||
// nothing closes, so it gains nothing over a kill.
|
// next delivery the worker archives.
|
||||||
func (e *Engine) stop(ctx context.Context) error {
|
func (e *Engine) stop(ctx context.Context) error {
|
||||||
e.log.Info("delivery engine stopping")
|
e.log.Info("delivery engine stopping")
|
||||||
|
|
||||||
|
|||||||
@@ -327,10 +327,10 @@ func TestEngine_StopHookClosesArchives(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestEngine_StopHookTimeoutLeavesArchivesOpen covers a stop whose
|
// TestEngine_StopHookTimeoutLeavesArchivesOpen covers a stop whose
|
||||||
// budget runs out while a worker is still running. The archive
|
// budget runs out while a worker is still running. That worker may
|
||||||
// writers are left open, as a kill would leave them: closing them
|
// be in the middle of an archive write, so the archive writers are
|
||||||
// would wait for any write in progress, and that worker would then
|
// left open, as a kill would leave them, rather than closed
|
||||||
// open new writers that nothing closes.
|
// underneath it.
|
||||||
func TestEngine_StopHookTimeoutLeavesArchivesOpen(t *testing.T) {
|
func TestEngine_StopHookTimeoutLeavesArchivesOpen(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -1166,10 +1166,6 @@ func TestIsForwardableHeader(t *testing.T) {
|
|||||||
assert.False(t,
|
assert.False(t,
|
||||||
delivery.ExportIsForwardableHeader("Content-Length"),
|
delivery.ExportIsForwardableHeader("Content-Length"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.False(t,
|
|
||||||
delivery.ExportIsForwardableHeader("Content-Type"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTruncate(t *testing.T) {
|
func TestTruncate(t *testing.T) {
|
||||||
@@ -1251,81 +1247,6 @@ func TestDoHTTPRequest_ForwardsHeaders(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The event's stored inbound headers carry the same Content-Type the
|
|
||||||
// receiver saved as the event's ContentType, so a delivery could send
|
|
||||||
// it twice. It must go out exactly once, with a Content-Type configured
|
|
||||||
// on the target winning, then the event's ContentType.
|
|
||||||
func TestApplyRequestHeaders_SendsOneContentType(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := map[string]struct {
|
|
||||||
inbound string
|
|
||||||
event string
|
|
||||||
configured string
|
|
||||||
want []string
|
|
||||||
}{
|
|
||||||
"inbound and event agree": {
|
|
||||||
inbound: testContentType,
|
|
||||||
event: testContentType,
|
|
||||||
want: []string{testContentType},
|
|
||||||
},
|
|
||||||
"inbound and event disagree": {
|
|
||||||
inbound: "text/plain",
|
|
||||||
event: testContentType,
|
|
||||||
want: []string{testContentType},
|
|
||||||
},
|
|
||||||
"event has none": {
|
|
||||||
inbound: testContentType,
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
"target configures its own": {
|
|
||||||
inbound: testContentType,
|
|
||||||
event: testContentType,
|
|
||||||
configured: "application/xml",
|
|
||||||
want: []string{"application/xml"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range cases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
inbound, err := json.Marshal(map[string][]string{
|
|
||||||
headerContentType: {tc.inbound},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cfg := &delivery.HTTPTargetConfig{}
|
|
||||||
if tc.configured != "" {
|
|
||||||
cfg.Headers = map[string]string{
|
|
||||||
headerContentType: tc.configured,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
context.Background(),
|
|
||||||
http.MethodPost,
|
|
||||||
"https://target.example.com/hook",
|
|
||||||
http.NoBody,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
delivery.ExportApplyRequestHeaders(
|
|
||||||
req,
|
|
||||||
&database.Event{
|
|
||||||
Headers: string(inbound),
|
|
||||||
ContentType: tc.event,
|
|
||||||
},
|
|
||||||
cfg,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.Equal(t,
|
|
||||||
tc.want, req.Header.Values(headerContentType),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProcessDelivery_RoutesToCorrectHandler(
|
func TestProcessDelivery_RoutesToCorrectHandler(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -339,11 +339,10 @@ func TestRedirectPolicy_StopsAtHopCap(t *testing.T) {
|
|||||||
// The set the redirect policy strips is whatever the delivery path
|
// The set the redirect policy strips is whatever the delivery path
|
||||||
// actually put on the wire, so a header added to the forward set is
|
// actually put on the wire, so a header added to the forward set is
|
||||||
// covered without a second edit. A header the event never carried
|
// covered without a second edit. A header the event never carried
|
||||||
// is not in the set, and neither is the inbound Content-Type, because
|
// is not in the set, and the delivery path's own two are deliberately
|
||||||
// it is not forwarded. Two more are deliberately excluded: a
|
// excluded: Content-Type describes the body, which a 307 carries
|
||||||
// Content-Type configured on the target describes the body, which a
|
// across hosts, and the inbound User-Agent every real sender supplies
|
||||||
// 307 carries across hosts, and the inbound User-Agent every real
|
// is overwritten before the request goes out.
|
||||||
// sender supplies is overwritten before the request goes out.
|
|
||||||
func TestApplyRequestHeaders_ReportsOriginScopedNames(t *testing.T) {
|
func TestApplyRequestHeaders_ReportsOriginScopedNames(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -372,7 +371,6 @@ func TestApplyRequestHeaders_ReportsOriginScopedNames(t *testing.T) {
|
|||||||
&delivery.HTTPTargetConfig{
|
&delivery.HTTPTargetConfig{
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
probeHeaderName: probeHeaderValue,
|
probeHeaderName: probeHeaderValue,
|
||||||
"Content-Type": testContentType,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -380,11 +378,7 @@ func TestApplyRequestHeaders_ReportsOriginScopedNames(t *testing.T) {
|
|||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{probeHeaderName, inboundHeaderName}, names,
|
[]string{probeHeaderName, inboundHeaderName}, names,
|
||||||
"both header classes are reported, and only those: "+
|
"both header classes are reported, and only those: "+
|
||||||
"Host and the inbound Content-Type are never "+
|
"Host is never forwarded, Content-Type and "+
|
||||||
"forwarded, User-Agent is the delivery path's own",
|
"User-Agent are the delivery path's own",
|
||||||
)
|
|
||||||
assert.NotContains(t, names, "Content-Type",
|
|
||||||
"a Content-Type configured on the target must survive "+
|
|
||||||
"a cross-origin 307/308 with the body it describes",
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ var (
|
|||||||
"hostname resolved to no IP addresses",
|
"hostname resolved to no IP addresses",
|
||||||
)
|
)
|
||||||
errBlockedIP = errors.New(
|
errBlockedIP = errors.New(
|
||||||
"blocked private, reserved or cloud metadata address",
|
"blocked private/reserved IP range",
|
||||||
)
|
)
|
||||||
errBlockedMetadata = errors.New(
|
errBlockedMetadata = errors.New(
|
||||||
"blocked link-local or cloud instance metadata " +
|
"blocked link-local or cloud instance metadata " +
|
||||||
@@ -37,18 +37,11 @@ var (
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// blockedNetworks is the default blocklist: the private and
|
// blockedNetworks contains all private/reserved IP ranges
|
||||||
// reserved IP ranges, plus the public cloud metadata addresses,
|
// that should be blocked to prevent SSRF attacks. An operator
|
||||||
// that are blocked to prevent SSRF attacks. An operator can
|
// can permit specific blocks out of this set with
|
||||||
// permit specific blocks out of this set with
|
|
||||||
// ALLOWED_EGRESS_CIDRS; see Guard.
|
// ALLOWED_EGRESS_CIDRS; see Guard.
|
||||||
//
|
//
|
||||||
// A public address belongs here only if it serves cloud
|
|
||||||
// credentials; a provider's other services on public addresses,
|
|
||||||
// such as its DNS resolvers or package mirrors, stay out, since
|
|
||||||
// reaching them can be legitimate and no list of them could be
|
|
||||||
// complete.
|
|
||||||
//
|
|
||||||
//nolint:gochecknoglobals // package-level network list is appropriate here
|
//nolint:gochecknoglobals // package-level network list is appropriate here
|
||||||
var blockedNetworks []*net.IPNet
|
var blockedNetworks []*net.IPNet
|
||||||
|
|
||||||
@@ -129,8 +122,6 @@ func init() {
|
|||||||
"::1/128",
|
"::1/128",
|
||||||
"fc00::/7",
|
"fc00::/7",
|
||||||
"fe80::/10",
|
"fe80::/10",
|
||||||
// Azure WireServer, a public address that serves VM credentials.
|
|
||||||
"168.63.129.16/32",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Every entry is named. The set must not grow or shrink
|
// Every entry is named. The set must not grow or shrink
|
||||||
@@ -225,8 +216,8 @@ func matchesAny(networks []*net.IPNet, ip net.IP) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isBlockedIP checks whether an IP address falls within
|
// isBlockedIP checks whether an IP address falls within
|
||||||
// the default blocklist, before any operator allowlist is
|
// any blocked private/reserved network range, before any
|
||||||
// considered.
|
// operator allowlist is considered.
|
||||||
func isBlockedIP(ip net.IP) bool {
|
func isBlockedIP(ip net.IP) bool {
|
||||||
return matchesAny(blockedNetworks, ip)
|
return matchesAny(blockedNetworks, ip)
|
||||||
}
|
}
|
||||||
@@ -329,7 +320,7 @@ func (g *Guard) allows(ip net.IP) bool {
|
|||||||
//
|
//
|
||||||
// 1. alwaysBlockedNetworks is refused before the allowlist is
|
// 1. alwaysBlockedNetworks is refused before the allowlist is
|
||||||
// consulted, so no configured CIDR reaches link-local or a
|
// consulted, so no configured CIDR reaches link-local or a
|
||||||
// cloud metadata endpoint at a non-public address.
|
// cloud instance metadata endpoint.
|
||||||
// 2. The allowlist is consulted next, so a listed private
|
// 2. The allowlist is consulted next, so a listed private
|
||||||
// network becomes reachable.
|
// network becomes reachable.
|
||||||
// 3. Everything else keeps the default blocklist's answer.
|
// 3. Everything else keeps the default blocklist's answer.
|
||||||
|
|||||||
@@ -390,41 +390,6 @@ func TestGuardAllowlist_PublicUnaffected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGuardAllowlist_AzureWireServerReopenable covers Azure's
|
|
||||||
// WireServer, a public address that serves VM credentials. The
|
|
||||||
// default guard refuses it, but because it is public it sits in
|
|
||||||
// the default blocklist rather than the unconditional set, so an
|
|
||||||
// operator who lists it can reach it.
|
|
||||||
func TestGuardAllowlist_AzureWireServerReopenable(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const wireServerIP = "168.63.129.16"
|
|
||||||
|
|
||||||
target := "http://" + wireServerIP + "/?comp=versions"
|
|
||||||
|
|
||||||
defaultGuard := delivery.NewTestGuard()
|
|
||||||
|
|
||||||
err := defaultGuard.ValidateTargetURL(context.Background(), target)
|
|
||||||
require.Error(t, err,
|
|
||||||
"WireServer must be refused with no allowlist set",
|
|
||||||
)
|
|
||||||
assert.NotContains(t, err.Error(), metadataRefusalClause,
|
|
||||||
"WireServer must be refused by the default blocklist, "+
|
|
||||||
"which an allowlist can override",
|
|
||||||
)
|
|
||||||
|
|
||||||
assertDialRefused(t, defaultGuard, target)
|
|
||||||
|
|
||||||
listed := delivery.NewTestGuard(
|
|
||||||
netip.MustParsePrefix(wireServerIP + "/32"),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.NoError(t,
|
|
||||||
listed.ValidateTargetURL(context.Background(), target),
|
|
||||||
"an operator who lists WireServer must be able to reach it",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGuardCheckIP_BothPathsShareOneDecision asserts that the
|
// TestGuardCheckIP_BothPathsShareOneDecision asserts that the
|
||||||
// validator and the dialer are not two policies that happen to
|
// validator and the dialer are not two policies that happen to
|
||||||
// agree: both are defined in terms of checkIP, so the exported
|
// agree: both are defined in terms of checkIP, so the exported
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/delivery"
|
"sneak.berlin/go/webhooker/internal/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Literals these tests repeat, named so that the header names and the
|
// Literals these tests repeat, named so that the header name and the
|
||||||
// keep-forever archive config each have one definition.
|
// keep-forever archive config each have one definition.
|
||||||
const (
|
const (
|
||||||
headerAuthorization = "Authorization"
|
headerAuthorization = "Authorization"
|
||||||
headerContentType = "Content-Type"
|
|
||||||
bearerValue = "Bearer abc"
|
bearerValue = "Bearer abc"
|
||||||
archiveConfigNever = "{\"expiry\":\"never\"}"
|
archiveConfigNever = "{\"expiry\":\"never\"}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -541,11 +541,6 @@ func isForwardableHeader(name string) bool {
|
|||||||
"Upgrade", "Proxy-Authorization",
|
"Upgrade", "Proxy-Authorization",
|
||||||
"Proxy-Connection", "Content-Length":
|
"Proxy-Connection", "Content-Length":
|
||||||
return false
|
return false
|
||||||
case "Content-Type":
|
|
||||||
// applyRequestHeaders sets Content-Type itself. The receiver
|
|
||||||
// already stored this inbound value as the event's
|
|
||||||
// ContentType, so forwarding it too would send it twice.
|
|
||||||
return false
|
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -558,10 +553,6 @@ func isForwardableHeader(name string) bool {
|
|||||||
// policy strips exactly that set on a hop that leaves the origin,
|
// policy strips exactly that set on a hop that leaves the origin,
|
||||||
// so the forward set is decided here and only here — a header added
|
// so the forward set is decided here and only here — a header added
|
||||||
// to it is covered off-origin without a second edit elsewhere.
|
// to it is covered off-origin without a second edit elsewhere.
|
||||||
//
|
|
||||||
// Content-Type goes out once: a Content-Type configured on the target
|
|
||||||
// wins, otherwise the event's ContentType, otherwise none. The inbound
|
|
||||||
// Content-Type in the event's headers is never forwarded.
|
|
||||||
func applyRequestHeaders(
|
func applyRequestHeaders(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
event *database.Event,
|
event *database.Event,
|
||||||
@@ -582,10 +573,10 @@ func applyRequestHeaders(
|
|||||||
|
|
||||||
req.Header.Set("User-Agent", "webhooker/1.0")
|
req.Header.Set("User-Agent", "webhooker/1.0")
|
||||||
|
|
||||||
// A Content-Type configured on the target describes the body
|
// Content-Type describes the body being sent rather than the
|
||||||
// being sent rather than the sender. A 307/308 preserves the
|
// sender, and the delivery path sets it from the event itself.
|
||||||
// body across hosts, so stripping it would send that body
|
// A 307/308 preserves the body across hosts, so stripping it
|
||||||
// untyped.
|
// would send that body untyped.
|
||||||
delete(originScoped, "Content-Type")
|
delete(originScoped, "Content-Type")
|
||||||
|
|
||||||
// User-Agent is overwritten just above, so an inbound one never
|
// User-Agent is overwritten just above, so an inbound one never
|
||||||
|
|||||||
@@ -133,11 +133,6 @@ func (w *recoverResponseWriter) Unwrap() http.ResponseWriter {
|
|||||||
// what the access log records and the metrics count, and outside the
|
// what the access log records and the metrics count, and outside the
|
||||||
// sentryhttp handler, whose Repanic option depends on something
|
// sentryhttp handler, whose Repanic option depends on something
|
||||||
// further out recovering what it re-raises.
|
// further out recovering what it re-raises.
|
||||||
//
|
|
||||||
// Unlike http.Error on its own, it deletes any Set-Cookie the handler
|
|
||||||
// set before panicking, because a request that failed must not hand
|
|
||||||
// the client a credential; every other header is left to http.Error.
|
|
||||||
// See https://git.eeqj.de/sneak/webhooker/issues/193.
|
|
||||||
func (s *Middleware) Recoverer() func(http.Handler) http.Handler {
|
func (s *Middleware) Recoverer() func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(
|
return http.HandlerFunc(func(
|
||||||
@@ -169,8 +164,6 @@ func (s *Middleware) Recoverer() func(http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rw.Header().Del("Set-Cookie")
|
|
||||||
|
|
||||||
http.Error(
|
http.Error(
|
||||||
rw,
|
rw,
|
||||||
http.StatusText(
|
http.StatusText(
|
||||||
|
|||||||
@@ -304,44 +304,16 @@ func TestRecovererRepanicsErrAbortHandler(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRecovererDropsSetCookieFromTheRecovered500 covers a handler that
|
|
||||||
// sets a cookie and a redirect target and then panics before sending
|
|
||||||
// anything. A request that failed must not hand the client a
|
|
||||||
// credential, so the 500 carries no cookie; Location is left alone.
|
|
||||||
func TestRecovererDropsSetCookieFromTheRecovered500(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
probe := newRecovererProbe(
|
|
||||||
t, false,
|
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.Header().Set("Set-Cookie", "session=x")
|
|
||||||
w.Header().Set("Location", "/after")
|
|
||||||
|
|
||||||
panic(panicMarker)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
resp, err := probe.get(t)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, resp.Body.Close())
|
|
||||||
|
|
||||||
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
|
||||||
assert.Empty(t, resp.Cookies())
|
|
||||||
assert.Equal(t, "/after", resp.Header.Get("Location"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRecovererKeepsAnAlreadyCommittedResponse covers a handler that
|
// TestRecovererKeepsAnAlreadyCommittedResponse covers a handler that
|
||||||
// panics after sending its status. The bytes are already on the wire,
|
// panics after sending its status. The bytes are already on the wire,
|
||||||
// cookie included, so a second WriteHeader would change nothing the
|
// so a second WriteHeader would change nothing the client sees and
|
||||||
// client sees and would draw net/http's "superfluous
|
// would draw net/http's "superfluous response.WriteHeader" report.
|
||||||
// response.WriteHeader" report.
|
|
||||||
func TestRecovererKeepsAnAlreadyCommittedResponse(t *testing.T) {
|
func TestRecovererKeepsAnAlreadyCommittedResponse(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
probe := newRecovererProbe(
|
probe := newRecovererProbe(
|
||||||
t, false,
|
t, false,
|
||||||
func(w http.ResponseWriter, _ *http.Request) {
|
func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set("Set-Cookie", "session=x")
|
|
||||||
w.WriteHeader(committedStatus)
|
w.WriteHeader(committedStatus)
|
||||||
_, _ = w.Write([]byte("partial"))
|
_, _ = w.Write([]byte("partial"))
|
||||||
|
|
||||||
@@ -359,7 +331,6 @@ func TestRecovererKeepsAnAlreadyCommittedResponse(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, committedStatus, resp.StatusCode)
|
assert.Equal(t, committedStatus, resp.StatusCode)
|
||||||
assert.Equal(t, "partial", string(body))
|
assert.Equal(t, "partial", string(body))
|
||||||
assert.Len(t, resp.Cookies(), 1)
|
|
||||||
|
|
||||||
record := probe.panicRecord(t)
|
record := probe.panicRecord(t)
|
||||||
assert.Equal(t, panicMarker, record["panic"])
|
assert.Equal(t, panicMarker, record["panic"])
|
||||||
|
|||||||
Reference in New Issue
Block a user