Overview
CVE-2026-44116 describes a server-side request forgery in OpenClaw's Zalo plugin's sendPhoto function, where the SSRF guard failed to validate outbound photo URLs. An attacker could supply malicious URLs to the Zalo Bot API, causing the server to fetch internal resources that would otherwise be inaccessible. This vulnerability aligns with CWE-918: Server-Side Request Forgery. The real-world impact includes access to internal services, cloud metadata endpoints, and other protected resources, enabling data leakage or further network pivoting. In practice, such flaws arise when a Go (Gin) service fetches or proxies remote content based on user-provided URLs without strict validation or egress controls, allowing attackers to trigger requests to internal or restricted networks via the server.
Affected Versions
OpenClaw before 2026.4.22
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type PhotoRequest struct {
PhotoURL string
}
func main() {
r := gin.Default()
// Vulnerable endpoint (demonstration)
r.POST("/vuln/photo", vulnerableHandler)
// Fixed endpoint (recommended)
r.POST("/v1/photo", secureHandler)
r.Run()
}
func vulnerableHandler(c *gin.Context) {
var req PhotoRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "bad request"})
return
}
// Vulnerable: directly fetch user-provided URL
resp, err := http.Get(req.PhotoURL)
if err != nil {
c.JSON(502, gin.H{"error": "fetch failed"})
return
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), data)
}
func isUnsafeURL(u *url.URL) bool {
if u.Scheme != "http" && u.Scheme != "https" {
return true
}
host := strings.ToLower(u.Hostname())
if host == "localhost" {
return true
}
if strings.HasPrefix(host, "127.0.0.") {
return true
}
if strings.HasPrefix(host, "10.") {
return true
}
if strings.HasPrefix(host, "172.") {
return true
}
if strings.HasPrefix(host, "192.168.") {
return true
}
return false
}
func secureHandler(c *gin.Context) {
var req PhotoRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "bad request"})
return
}
parsed, err := url.Parse(req.PhotoURL)
if err != nil {
c.JSON(400, gin.H{"error": "invalid url"})
return
}
if isUnsafeURL(parsed) {
c.JSON(400, gin.H{"error": "unsafe url"})
return
}
transport := &http.Transport{}
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(req.PhotoURL)
if err != nil {
c.JSON(502, gin.H{"error": "fetch failed"})
return
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), data)
}