{"record":{"id":"fa253df244b4ba44","repo":"usememos/memos","slug":"internal-ip-addresses-are-not-allowed","errorCode":null,"errorMessage":"internal IP addresses are not allowed","messagePattern":"internal IP addresses are not allowed","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"internal/httpgetter/html_meta.go","lineNumber":18,"sourceCode":"package httpgetter\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/net/html\"\n\t\"golang.org/x/net/html/atom\"\n)\n\nvar ErrInternalIP = errors.New(\"internal IP addresses are not allowed\")\n\nconst maxHTMLMetaBytes = 512 * 1024\n\nvar (\n\tlookupIPAddr = net.DefaultResolver.LookupIPAddr\n\tdialContext  = (&net.Dialer{\n\t\tTimeout:   30 * time.Second,\n\t\tKeepAlive: 30 * time.Second,\n\t}).DialContext\n\thttpClient = newHTTPClient()\n)\n\nfunc newHTTPClient() *http.Client {\n\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\ttransport.Proxy = nil\n\ttransport.DialContext = secureDialContext\n\n\treturn &http.Client{","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/internal/httpgetter/html_meta.go#L1-L36","documentation":"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).","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":"// before\nhttpgetter.GetHTMLMeta(\"http://192.168.1.10:8080/page\")\n// after\nhttpgetter.GetHTMLMeta(\"https://example.com/page\")","handlingStrategy":"validation","validationCode":"// Pre-check a URL is externally resolvable before fetching metadata\nfunc isFetchableURL(raw string) error {\n  u, err := url.Parse(raw)\n  if err != nil { return err }\n  if u.Scheme != \"http\" && u.Scheme != \"https\" { return errors.New(\"bad scheme\") }\n  host := u.Hostname()\n  if ip := net.ParseIP(host); ip != nil {\n    if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {\n      return errors.New(\"internal IP\")\n    }\n    return nil\n  }\n  addrs, err := net.DefaultResolver.LookupIPAddr(context.Background(), host)\n  if err != nil { return err }\n  for _, a := range addrs {\n    if a.IP == nil || a.IP.IsLoopback() || a.IP.IsPrivate() || a.IP.IsLinkLocalUnicast() || a.IP.IsUnspecified() {\n      return errors.New(\"resolves to internal IP\")\n    }\n  }\n  return nil\n}","typeGuard":null,"tryCatchPattern":"// Detect the sentinel and skip preview generation instead of failing the request\nif err := httpgetter.GetHTMLMeta(u); err != nil {\n  if errors.Is(err, httpgetter.ErrInternalIP) { /* skip preview, keep note */ return nil }\n  return err\n}","preventionTips":["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"],"tags":["network","ssrf","security","dns"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}