Skip to content

pkg/name: unanchored loopback regex causes TLS downgrade for hosts containing "127.0.0.1" or "::1" substring #2313

Description

@moraneus

Summary

pkg/name/registry.go uses unanchored regular expressions to detect
loopback addresses for the purpose of choosing HTTP vs HTTPS scheme.
Because regexp.QuoteMeta only escapes metacharacters and does not
add ^/$ anchors, the regex matches the literal substring
127.0.0.1 (or ::1) anywhere in the hostname. Any registry hostname
that embeds either substring is misclassified as loopback and
contacted over plaintext HTTP instead of HTTPS.

This was reported to the Google OSS-VRP and acknowledged as a valid
security concern (TLS downgrade / potential data exposure) but
classified as a product vulnerability under OT2/OT3 — Google
suggested I file it here directly. Hence this issue.

Affected code

pkg/name/registry.go lines 30-34:

// Detect the loopback IP (127.0.0.1)
var reLoopback     = regexp.MustCompile(regexp.QuoteMeta("127.0.0.1"))

// Detect the loopback IPV6 (::1)
var reipv6Loopback = regexp.MustCompile(regexp.QuoteMeta("::1"))

Compare the sibling reLocal on the same file, which IS properly
anchored — so the existing convention in this file is to anchor:

var reLocal = regexp.MustCompile(`.*\.localhost(?::\d{1,5})?$`)

Reproducer

package main

import (
      "fmt"

      "github.com/google/go-containerregistry/pkg/name"
)

func main() {
      for _, h := range []string{
              "127.0.0.1",                 // intended loopback, should be http
              "127.0.0.1:5000",            // intended loopback, should be http
              "[::1]:5000",                // intended loopback, should be http
              "127.0.0.1.evil.com",        // NOT loopback, should be https
              "127.0.0.1.evil.com:5000",   // NOT loopback, should be https
              "my127.0.0.1registry.io",    // NOT loopback, should be https
              "evil.com.127.0.0.1.nip.io", // NOT loopback, should be https
              "x127.0.0.1x.com",           // NOT loopback, should be https
              "[2001:db8::1]:5000",        // global IPv6, should be https
      } {
              r, err := name.NewRegistry(h)
              if err != nil {
                      fmt.Printf("%q rejected: %v\n", h, err)
                      continue
              }
              fmt.Printf("%q -> %s\n", h, r.Scheme())
      }
} 

Output against v0.21.6:

"127.0.0.1" -> http
"127.0.0.1:5000" -> http
"[::1]:5000" -> http
"127.0.0.1.evil.com" -> http        <-- BUG (should be https)
"127.0.0.1.evil.com:5000" -> http   <-- BUG
"my127.0.0.1registry.io" -> http    <-- BUG
"evil.com.127.0.0.1.nip.io" -> http <-- BUG
"x127.0.0.1x.com" -> http           <-- BUG
"[2001:db8::1]:5000" -> http        <-- BUG

Versions confirmed vulnerable

  • v0.21.6 (latest released at time of report)
  • master @ commit d807d512681c6b18391a8fa0ae0fb27c36ba5f4d (2026-05-19)

Impact

Any consumer of pkg/name that uses Registry.Scheme() to construct
outbound HTTP requests — which includes crane, cosign, kpack,
ko, skaffold, and any tool that takes user-supplied image
references. An attacker controlling a hostname that embeds the
substring (trivial via wildcard DNS or owning any domain such as
127.0.0.1.attacker.tld) can force the client to use plaintext HTTP,
exposing bearer tokens / basic-auth credentials on the wire and
enabling on-path tampering of manifest/layer responses.

For IPv6 the issue is broader: any address whose zero-compression
places ::1 anywhere in the textual form (2001:db8::1, fe80::1,
fec0::1, etc.) is misclassified.

Suggested fix

Either anchor the regexes:

var reLoopback     = regexp.MustCompile(`^127\.0\.0\.1(?::\d{1,5})?$`)
var reipv6Loopback = regexp.MustCompile(`^\[::1\](?::\d{1,5})?$`)

Or, preferably, drop the regex and use structured parsing — this
also fixes a related weakness in isRFC1918() which uses
strings.Split(r.Name(), ":")[0] for port stripping and does not
handle bracketed IPv6 hosts:

func (r Registry) isLoopback() bool {
    h := r.Name()
    // Strip port and IPv6 brackets, then parse as IP.
    if i := strings.LastIndex(h, ":"); i >= 0 && !strings.HasSuffix(h[:i], "]") {
        h = h[:i] 
    }
    h = strings.TrimSuffix(strings.TrimPrefix(h, "["), "]")
    if ip := net.ParseIP(h); ip != nil {
        return ip.IsLoopback()
    }
    return false
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions