CVE-2025-22952
Description
elestio memos v0.23.0 is vulnerable to Server-Side Request Forgery (SSRF) due to insufficient validation of user-supplied URLs, which can be exploited to perform SSRF attacks.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Memos v0.23.0 fails to validate user-supplied URLs, allowing SSRF attacks against internal networks and services.
The vulnerability [CVE-2025-22952] affects elestio memos v0.23.0 and stems from insufficient validation of user-supplied URLs in the application's web link preview and metadata retrieval functionalities. The root cause is that the system does not properly restrict which IP addresses or protocols can be contacted when processing a user-provided URL, enabling attackers to bypass intended access controls [1][4].
A remote attacker can exploit this SSRF flaw by sending crafted HTTP requests that include internal IP addresses (such as 127.0.0.1 or 172.17.0.1) or other non-standard protocols to the affected application. No special privileges are required beyond the ability to submit a URL for preview. The application will then fetch the target URL, allowing the attacker to probe internal services and ports that are not directly accessible from the external network [2][4].
The impact is significant: an attacker can scan internal network hosts, access sensitive endpoints (such as cloud metadata services or internal admin panels), and potentially enumerate or interact with weak internal services. The vulnerability enables the attacker to effectively pivot from the application server into the internal network, compromising confidentiality and integrity of internal resources [1][4].
Mitigation is available via pull requests that add validation and block internal network URLs. Specifically, PR #4421 introduces checks to prevent previewing internal network web pages, and PR #4428 addresses redirect attacks in the GetLinkMetadata API [1][3]. Users are strongly advised to update to the latest patched version or apply the proposed fixes to their deployment.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/usememos/memosGo | <= 0.24.0 | — |
Affected products
1- elestio/memosdescription
Patches
2f17774cb3b96feat: prevent attackers from exploiting redirect attack GetLinkMetadata API (#4428)
2 files changed · +58 −8
plugin/httpgetter/html_meta.go+38 −8 modified@@ -1,16 +1,30 @@ package httpgetter import ( - "errors" "io" "net" "net/http" "net/url" + "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") + +var httpClient = &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if err := validateURL(req.URL.String()); err != nil { + return errors.Wrap(err, "redirect to internal IP") + } + if len(via) >= 10 { + return errors.New("too many redirects") + } + return nil + }, +} + type HTMLMeta struct { Title string `json:"title"` Description string `json:"description"` @@ -22,7 +36,7 @@ func GetHTMLMeta(urlStr string) (*HTMLMeta, error) { return nil, err } - response, err := http.Get(urlStr) + response, err := httpClient.Get(urlStr) if err != nil { return nil, err } @@ -110,12 +124,28 @@ func validateURL(urlStr string) error { return errors.New("only http/https protocols are allowed") } - if host := u.Hostname(); host != "" { - ip := net.ParseIP(host) - if ip != nil { - if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() { - return errors.New("internal IP addresses are not allowed") - } + host := u.Hostname() + if host == "" { + return errors.New("empty hostname") + } + + // check if the hostname is an IP + if ip := net.ParseIP(host); ip != nil { + if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() { + return errors.Wrap(ErrInternalIP, ip.String()) + } + return nil + } + + // check if it's a hostname, resolve it and check all returned IPs + ips, err := net.LookupIP(host) + if err != nil { + return errors.Errorf("failed to resolve hostname: %v", err) + } + + for _, ip := range ips { + if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() { + return errors.Wrapf(ErrInternalIP, "host=%s, ip=%s", host, ip.String()) } }
plugin/httpgetter/html_meta_test.go+20 −0 modified@@ -1,6 +1,8 @@ package httpgetter import ( + "errors" + "strings" "testing" "github.com/stretchr/testify/require" @@ -17,3 +19,21 @@ func TestGetHTMLMeta(t *testing.T) { require.Equal(t, test.htmlMeta, *metadata) } } + +func TestGetHTMLMetaForInternal(t *testing.T) { + // test for internal IP + if _, err := GetHTMLMeta("http://192.168.0.1"); !errors.Is(err, ErrInternalIP) { + t.Errorf("Expected error for internal IP, got %v", err) + } + + // test for resolved internal IP + if _, err := GetHTMLMeta("http://localhost"); !errors.Is(err, ErrInternalIP) { + t.Errorf("Expected error for resolved internal IP, got %v", err) + } + + // test for redirected internal IP + // 49.232.126.226:1110 will redirects to 127.0.0.1 + if _, err := GetHTMLMeta("http://49.232.126.226:1110"); !(errors.Is(err, ErrInternalIP) && strings.Contains(err.Error(), "redirect")) { + t.Errorf("Expected error for redirected internal IP, got %v", err) + } +}
f8c973c93874fix: prevent previewing internal network web pages. (#4421)
1 file changed · +26 −1
plugin/httpgetter/html_meta.go+26 −1 modified@@ -3,6 +3,7 @@ package httpgetter import ( "errors" "io" + "net" "net/http" "net/url" @@ -17,7 +18,7 @@ type HTMLMeta struct { } func GetHTMLMeta(urlStr string) (*HTMLMeta, error) { - if _, err := url.Parse(urlStr); err != nil { + if err := validateURL(urlStr); err != nil { return nil, err } @@ -35,6 +36,8 @@ func GetHTMLMeta(urlStr string) (*HTMLMeta, error) { return nil, errors.New("not a HTML page") } + // TODO: limit the size of the response body + htmlMeta := extractHTMLMeta(response.Body) return htmlMeta, nil } @@ -96,3 +99,25 @@ func extractMetaProperty(token html.Token, prop string) (content string, ok bool } return content, ok } + +func validateURL(urlStr string) error { + u, err := url.Parse(urlStr) + if err != nil { + return errors.New("invalid URL format") + } + + if u.Scheme != "http" && u.Scheme != "https" { + return errors.New("only http/https protocols are allowed") + } + + if host := u.Hostname(); host != "" { + ip := net.ParseIP(host) + if ip != nil { + if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() { + return errors.New("internal IP addresses are not allowed") + } + } + } + + return nil +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-wfxg-v3j4-7qmjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-22952ghsaADVISORY
- elest.io/open-source/memosghsaWEB
- github.com/usememos/memos/commit/f17774cb3b9612495d89576a91ab3480018cb0b6ghsaWEB
- github.com/usememos/memos/commit/f8c973c938742827baaf6665cfe66805dc8e8d02ghsaWEB
- github.com/usememos/memos/issues/4413ghsaWEB
- github.com/usememos/memos/pull/4421ghsaWEB
- github.com/usememos/memos/pull/4428ghsaWEB
News mentions
1- ThreatsDay Bulletin: $290M DeFi Hack, macOS LotL Abuse, ProxySmart SIM Farms +25 New StoriesThe Hacker News · Apr 23, 2026