ory/hydra · error

ip %s is not a permitted destination

Error message

ip %s is not a permitted destination

What it means

IsAssociatedIPAllowed returns this error when the input parses as a bare IP address (netip.ParseAddr) and the allowed() predicate rejects it — i.e. the IP is not a permitted destination (typically because private/loopback/link-local ranges are disallowed as SSRF protection). Raised at ip_validator.go:55.

Source

Thrown at oryx/ipx/ip_validator.go:55

	return g.Wait()
}

// IsAssociatedIPAllowed returns nil for a domain (with NS lookup), IP, or IPv6 address if it
// does not resolve to a private IP subnet. This is a first level of defense against
// SSRF attacks by disallowing any domain or IP to resolve to a private network range.
//
// Please keep in mind that validations for domains is valid only when looking up.
// A malicious actor could easily update the DSN record post validation to point
// to an internal IP
func IsAssociatedIPAllowed(ctx context.Context, ipOrHostnameOrURL string) error {
	ipOrHostname := ipOrHostnameOrURL
	if parsed, err := url.ParseRequestURI(ipOrHostnameOrURL); err == nil {
		ipOrHostname = parsed.Hostname()
	}

	if ip, err := netip.ParseAddr(ipOrHostname); err == nil {
		if !allowed(ip) {
			return errors.Errorf("ip %s is not a permitted destination", ip)
		}
		return nil
	}

	if addr, err := netip.ParseAddrPort(ipOrHostnameOrURL); err == nil {
		if !allowed(addr.Addr()) {
			return errors.Errorf("ip %s is not a permitted destination", addr.Addr())
		}
		return nil
	}

	ctx, cancel := context.WithTimeoutCause(ctx, 2*time.Second, errors.New("DNS lookup timed out"))
	defer cancel()
	ips, err := resolver.LookupNetIP(ctx, "ip", ipOrHostname)
	if err != nil {
		if dnsErr, ok := stderrors.AsType[*net.DNSError](err); ok {
			// Copy the `*net.DNSError` before masking `Server` to avoid a data
			// race: the DNS resolver uses `singleflight` to deduplicate

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Use a public, internet-routable IP or hostname instead of the rejected address
  2. If the internal address is intentional, adjust the allowed() predicate / disallowed-range configuration to permit it
  3. In dev, expose the internal service via a public tunnel or the host's external address

Example fix

// before
err := ipx.IsAssociatedIPAllowed(ctx, "127.0.0.1:9000") // loopback rejected
// after
err := ipx.IsAssociatedIPAllowed(ctx, "93.184.216.34:9000") // public IP accepted
Defensive patterns

Strategy: validation

Validate before calling

func isPublicIP(s string) bool {
    ip, err := netip.ParseAddr(s)
    if err != nil { return false }
    return ip.IsGlobalUnicast() && !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
}

Try / catch

if err := ipx.IsAssociatedIPAllowed(ctx, input); err != nil {
    if strings.Contains(err.Error(), "not a permitted destination") {
        return fmt.Errorf("endpoint %s is internal/blocked; use a public address", input)
    }
    return err
}

Prevention

When it happens

Trigger: Calling IsAssociatedIPAllowed (directly, via IsAssociatedIPAllowedWhenSet, or via AreAllAssociatedIPsAllowed) with a string like "127.0.0.1", "10.0.0.5", "::1", or any IPv4/IPv6 address that the configured allowed() check rejects.

Common situations: Configuring callbacks, webhooks, or endpoints pointing at localhost/internal cluster IPs; Docker/K8s environments where services resolve to private ranges; SSRF guardrails rejecting internal addresses in submitted URLs.

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/b67ceff26968cc77. Report an issue: GitHub.