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
- Use a genuinely public URL for the fetch
- 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
- Check that the hostname does not resolve to RFC1918/link-local space: `dig +short <host>` or `nslookup <host>`
- 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
- Never point metadata fetching at localhost/private ranges, even in dev
- Use a public tunnel for local preview testing
- Compare with errors.Is against ErrInternalIP rather than string matching
- Remember redirects are re-validated: a public URL redirecting inward also fails
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
- hostname resolved to no addresses
- only http/https protocols are allowed
- The signed-in user changed before the OAuth callback complet
- too many redirects
- invalid URL format
AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15).
Data as JSON: /api/errors/fa253df244b4ba44.
Report an issue: GitHub.