usememos/memos · warning

internal IP addresses are not allowed

Error message

internal IP addresses are not allowed

What it means

SSRF guard in Memos' internal/httpgetter: every hostname used for metadata scraping is resolved, and if any resolved IP is loopback, private, link-local, or unspecified, ErrInternalIP is returned before any connection is made. This prevents the note-taking instance from being used to probe internal networks (e.g. cloud metadata at 169.254.169.254).

Source

Thrown at internal/httpgetter/html_meta.go:18

package httpgetter

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/net/html"
	"golang.org/x/net/html/atom"
)

var ErrInternalIP = errors.New("internal IP addresses are not allowed")

const maxHTMLMetaBytes = 512 * 1024

var (
	lookupIPAddr = net.DefaultResolver.LookupIPAddr
	dialContext  = (&net.Dialer{
		Timeout:   30 * time.Second,
		KeepAlive: 30 * time.Second,
	}).DialContext
	httpClient = newHTTPClient()
)

func newHTTPClient() *http.Client {
	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.Proxy = nil
	transport.DialContext = secureDialContext

	return &http.Client{

View on GitHub (pinned to 14d757ce1f)

Solutions

  1. Use a genuinely public URL for the fetch
  2. If the target is a legitimately internal service you control, host the metadata differently (fetch client-side, or through an allowlisted proxy) since the guard is intentional and not configurable
  3. Check that the hostname does not resolve to RFC1918/link-local space: `dig +short <host>` or `nslookup <host>`
  4. For local development, run the target on a public tunnel (e.g. a dev tunnel domain) instead of 127.0.0.1

Example fix

// before
httpgetter.GetHTMLMeta("http://192.168.1.10:8080/page")
// after
httpgetter.GetHTMLMeta("https://example.com/page")
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check a URL is externally resolvable before fetching metadata
func isFetchableURL(raw string) error {
  u, err := url.Parse(raw)
  if err != nil { return err }
  if u.Scheme != "http" && u.Scheme != "https" { return errors.New("bad scheme") }
  host := u.Hostname()
  if ip := net.ParseIP(host); ip != nil {
    if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
      return errors.New("internal IP")
    }
    return nil
  }
  addrs, err := net.DefaultResolver.LookupIPAddr(context.Background(), host)
  if err != nil { return err }
  for _, a := range addrs {
    if a.IP == nil || a.IP.IsLoopback() || a.IP.IsPrivate() || a.IP.IsLinkLocalUnicast() || a.IP.IsUnspecified() {
      return errors.New("resolves to internal IP")
    }
  }
  return nil
}

Try / catch

// Detect the sentinel and skip preview generation instead of failing the request
if err := httpgetter.GetHTMLMeta(u); err != nil {
  if errors.Is(err, httpgetter.ErrInternalIP) { /* skip preview, keep note */ return nil }
  return err
}

Prevention

When it happens

Trigger: Calling the link-preview/metadata fetch path with a URL whose host resolves to 10.x/172.16-31.x/192.168.x, 127.0.0.1, ::1, 0.0.0.0, or 169.254.x.x — including DNS names that resolve to private IPs. Also raised on redirects whose target resolves internally.

Common situations: Testing link previews against localhost or an internal service while developing; deploying behind a proxy where the target host's DNS returns private addresses; a public URL whose DNS is rebinded to an internal IP (DNS rebinding attempts are caught because resolution is re-checked per dial).

Related errors


AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15). Data as JSON: /api/errors/fa253df244b4ba44. Report an issue: GitHub.