+The Go module golang.org/x/net/idna implements UTS-46 IDNA
+processing. On the Lookup and Display profiles
+(and any profile constructed via idna.New(idna.MapForLookup(), ...)),
+both (*Profile).ToASCII and (*Profile).ToUnicode
+apply an NFKC-based character map that folds 100 distinct
+non-ASCII Unicode digit codepoints to their ASCII equivalents.
+The 100 codepoints partition into the following Unicode-block ranges:
+
+Devanagari digits (U+0966..U+096F) are not in scope:
+empirical testing against golang.org/x/net/idna v0.53.0
+confirms they do not fold to ASCII via UTS-46. The
+Registration profile is structurally covered by the rule
+but disallows every fold codepoint at the rune-validation stage, so a
+caller that respects the returned error never sees a
+smuggled literal from that profile in practice.
+
+The library contains no IP-literal detection. A caller that applies UTS-46
+mapping to an attacker-controlled host string and consumes the result in a
+network sink without rechecking against IP-literal parsers receives a
+valid ASCII IPv4 literal back as the "domain name" output. Any downstream
+allowlist check, SSRF guard, NoProxy match, or TLS-SNI router that does
+not re-check the post-IDNA result is bypassed. The anti-pattern also
+applies to callers that do a pre-IDNA net.ParseIP check and
+think it is sufficient: the smuggled host is not ASCII, so the pre-IDNA
+check rejects it as non-IP, and the post-IDNA value (now a numeric
+literal) reaches the sink unguarded.
+
+IPv6 is out of scope: : is a UTS-46 disallowed character;
+bare-IPv6 inputs are rejected by IDNA rune-validation before any
+digit-fold mapping runs.
+
+Sinks where the smuggled literal becomes exploitable include
+net.JoinHostPort, net.Dial,
+(*http.Request).URL.Host, (*tls.Config).ServerName,
+(*http.Cookie).Domain, and any HTTP client request URL
+constructed from the mapped value.
+
+Either: +
+net.ParseIP returns a non-nil address, or if
+netip.ParseAddr returns no error (note the inverted
+convention: netip.ParseAddr reports a successfully parsed
+address via err == nil, not via a non-zero return). The
+trailing-dot strip is required because "0.¹.0.0." maps to
+"0.1.0.0.", which a bare net.ParseIP rejects
+on its own yet is still an IP literal for routing purposes; the strip
+exposes the literal so the parser sees it.
++Vulnerable pattern. The host string is mapped through the IDNA profile +and reaches a network sink with no post-IDNA IP-literal recheck: +
+ +
+Safe pattern. Post-IDNA trailing-dot strip followed by
+net.ParseIP recheck:
+
+The safe pattern accepts three trailing-dot strip forms. They are +not equivalent in coverage: +
+strings.TrimRight(ace, "."): strict form. Strips
+ all trailing dots, so the multi-dot residue produced when UTS-46
+ maps the fullwidth dot U+FF0E or the ideographic dot U+3002 next
+ to ASCII dots is fully removed.strings.TrimSuffix(ace, "."): lenient form. Strips
+ only one trailing dot. Sufficient for the canonical
+ "0.1.0.0." shape but leaves residue if multiple
+ trailing dots were produced by mapping.if strings.HasSuffix(ace, ".") { ace = ace[:len(ace)-1] }:
+ manual single-dot slice. Behaves identically to
+ TrimSuffix in coverage and inherits the same
+ multi-dot-residue limitation.
+Callers whose threat model includes the multi-trailing-dot variant
+should prefer strings.TrimRight. After the strip, parse
+with netip.ParseAddr (preferred) or net.ParseIP
+and reject if the value parses as an IP literal (err == nil
+for the former, non-nil return for the latter).
+
golang.org/x/net/idna package documentation:
+https://pkg.go.dev/golang.org/x/net/idna
+ends_in_a_number host parser check
+(prior art for IP-literal detection in URL parsers):
+https://url.spec.whatwg.org/#ends-in-a-number-checker
+