mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-18 21:22:15 +01:00
fix(outbound): return 403 instead of 500 for an unresolvable outbound host
This commit is contained in:
@@ -338,8 +338,18 @@ func DecideOutbound(ctx context.Context, rawURL string, allowList, denyList []*r
|
|||||||
return OutboundDecision{}, fmt.Errorf("'%s' targets a non-public address: %w", normalized, ErrFiltered)
|
return OutboundDecision{}, fmt.Errorf("'%s' targets a non-public address: %w", normalized, ErrFiltered)
|
||||||
case errors.Is(err, ErrPublicIP):
|
case errors.Is(err, ErrPublicIP):
|
||||||
return OutboundDecision{}, fmt.Errorf("'%s' targets a public address: %w", normalized, ErrFiltered)
|
return OutboundDecision{}, fmt.Errorf("'%s' targets a public address: %w", normalized, ErrFiltered)
|
||||||
default:
|
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
|
||||||
|
// A cancellation or timeout is not a policy decision; surface it
|
||||||
|
// as-is so callers do not report it as a filtered request.
|
||||||
return OutboundDecision{}, fmt.Errorf("validate '%s' host: %w", normalized, err)
|
return OutboundDecision{}, fmt.Errorf("validate '%s' host: %w", normalized, err)
|
||||||
|
default:
|
||||||
|
// The host could not be resolved, so its address class cannot be
|
||||||
|
// verified. Fail closed and treat it as filtered, the same as a
|
||||||
|
// host that resolves to a blocked address, so clients get a
|
||||||
|
// generic 403 rather than a 500. This also denies alternate IP
|
||||||
|
// encodings such as http://2130706433/ that the resolver rejects
|
||||||
|
// as a hostname but Chromium would read as a private IP.
|
||||||
|
return OutboundDecision{}, fmt.Errorf("validate '%s' host: %v: %w", normalized, err, ErrFiltered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -311,6 +311,39 @@ func TestFilterOutboundURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecideOutbound_UnresolvableHostFailsClosed(t *testing.T) {
|
||||||
|
withStubResolver(t, func(string) ([]netip.Addr, error) {
|
||||||
|
return nil, errors.New("no such host")
|
||||||
|
})
|
||||||
|
|
||||||
|
// An alternate IP encoding (decimal for 127.0.0.1) that the resolver
|
||||||
|
// rejects as a hostname must fail closed as filtered, not surface as a
|
||||||
|
// server error, so clients receive a generic 403.
|
||||||
|
_, err := DecideOutbound(context.Background(), "http://2130706433/", nil, nil, time.Now().Add(5*time.Second), WithDenyPrivateIPs(true))
|
||||||
|
if !errors.Is(err, ErrFiltered) {
|
||||||
|
t.Fatalf("expected ErrFiltered, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecideOutbound_ResolverCancellationNotFiltered(t *testing.T) {
|
||||||
|
withStubResolver(t, func(string) ([]netip.Addr, error) {
|
||||||
|
return nil, context.Canceled
|
||||||
|
})
|
||||||
|
|
||||||
|
// A cancellation or timeout is not a policy decision and must not be
|
||||||
|
// reported as a filtered request.
|
||||||
|
_, err := DecideOutbound(context.Background(), "http://example.com/", nil, nil, time.Now().Add(5*time.Second), WithDenyPrivateIPs(true))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrFiltered) {
|
||||||
|
t.Fatalf("cancellation must not be filtered, got: %v", err)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatalf("expected context.Canceled, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveAndCheckPublic_IPLiteralLoopback(t *testing.T) {
|
func TestResolveAndCheckPublic_IPLiteralLoopback(t *testing.T) {
|
||||||
withStubResolver(t, func(host string) ([]netip.Addr, error) {
|
withStubResolver(t, func(host string) ([]netip.Addr, error) {
|
||||||
t.Fatalf("unexpected DNS lookup for %q", host)
|
t.Fatalf("unexpected DNS lookup for %q", host)
|
||||||
|
|||||||
Reference in New Issue
Block a user