How Server-Side Request Forgery (SSRF) Happens in Go HTTP Handlers and How to Fix It
The internal/web/controller/server.go file is responsible for handling web requests in this Go service — including an endpoint called applySubTemplate, which allows users to load configuration templates from a URL. A flaw in how that URL was handled created a textbook Server-Side Request Forgery (SSRF) vulnerability: the server would fetch any URL the user supplied, not just GitHub URLs as intended.
This post walks through exactly what went wrong, how an attacker could exploit it, and how an 11-line fix closes the door entirely.
The Vulnerability Explained
What the Code Was Doing
The applySubTemplate handler at line 340 of server.go accepted a form-bound struct containing a URL field and passed it directly to the service layer:
// BEFORE — vulnerable code (server.go ~line 348)
func (a *ServerController) applySubTemplate(c *gin.Context) {
// ... form binding ...
err := a.serverService.ApplySubTemplateFromGithub(f.URL)
if err != nil {
jsonMsg(c, err.Error(), nil)
return
}
jsonMsg(c, "Sub HTML template downloaded & installed successfully!", nil)
}
Despite the method being named ApplySubTemplateFromGithub, there was no enforcement that f.URL actually pointed to GitHub. The name was aspirational, not enforced. Any URL a user submitted — http://169.254.169.254/latest/meta-data/, http://internal-db:5432/, file:///etc/passwd — would be forwarded to the HTTP client without question.
Compounding the risk, a CheckRedirect handler at line 630 processed HTTP redirects without validating where those redirects led. This meant an attacker could submit a seemingly-legitimate URL that immediately redirected to an internal target, bypassing any naive string-level checks.
A Concrete Attack Scenario
Consider an attacker targeting this application running in AWS EC2. They craft a POST request to the applySubTemplate endpoint:
POST /api/server/applySubTemplate
Content-Type: application/x-www-form-urlencoded
url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
The server dutifully fetches the AWS Instance Metadata Service (IMDS) endpoint. The response — containing IAM role credentials — is then processed as if it were a GitHub template. Even if the processing fails gracefully, the server has already made the outbound request, and a crafty attacker can often infer the response contents through timing, error messages, or side channels.
A two-step redirect attack looks like this:
- Attacker hosts
https://attacker.com/redirectwhich returnsHTTP 302 → http://10.0.0.1/admin - Attacker submits
url=https://attacker.com/redirectto the endpoint - The
CheckRedirecthandler follows the redirect to the internal admin panel - The server fetches and processes internal content
Why This Is Classified CWE-918
CWE-918: Server-Side Request Forgery covers exactly this pattern: a web application fetches a remote resource based on user-supplied input without validating that the destination is within an expected, safe set of targets. The impact ranges from internal network reconnaissance to credential theft to full remote code execution in environments with metadata services or internal APIs.
The Fix
The patch introduces a dedicated validation function, isAllowedSubTemplateURL, and calls it before the service invocation. Here's the complete before/after:
Before (Vulnerable)
func (a *ServerController) applySubTemplate(c *gin.Context) {
var f struct{ URL string }
if err := c.ShouldBind(&f); err != nil {
jsonMsg(c, I18nWeb(c, "pages.server.loadError"), err)
return
}
// ❌ No URL validation — f.URL is used as-is
err := a.serverService.ApplySubTemplateFromGithub(f.URL)
...
}
After (Fixed)
func (a *ServerController) applySubTemplate(c *gin.Context) {
var f struct{ URL string }
if err := c.ShouldBind(&f); err != nil {
jsonMsg(c, I18nWeb(c, "pages.server.loadError"), err)
return
}
// ✅ Validate BEFORE calling the service
if !isAllowedSubTemplateURL(f.URL) {
jsonMsg(c, I18nWeb(c, "pages.server.loadError"), fmt.Errorf("only github.com and raw.githubusercontent.com URLs are allowed"))
return
}
err := a.serverService.ApplySubTemplateFromGithub(f.URL)
...
}
// isAllowedSubTemplateURL restricts sub-template downloads to GitHub hosts only,
// preventing SSRF via arbitrary or redirect-controlled destinations.
func isAllowedSubTemplateURL(rawURL string) bool {
u, err := url.Parse(rawURL)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
return false
}
host := strings.ToLower(u.Hostname())
return host == "github.com" || host == "raw.githubusercontent.com"
}
Why This Fix Works
Several design decisions in isAllowedSubTemplateURL are worth highlighting:
-
Proper URL parsing via
url.Parse()— Rather than substring matching (which can be bypassed withhttps://evil.com/github.com/path), the fix uses Go's standardnet/urlpackage to parse the URL and extract only the hostname component. -
Scheme enforcement — The function explicitly rejects anything that isn't
httporhttps. This blocksfile://,ftp://,gopher://, and other schemes that could be abused in certain HTTP client configurations. -
u.Hostname()notu.Host—Hostname()strips the port number before comparison, sogithub.com:443andgithub.comboth resolve togithub.com. Usingu.Hostdirectly would allowgithub.com.evil.comto be disguised as a port specification in some edge cases. -
Case normalization with
strings.ToLower()— Hostnames are case-insensitive per RFC 1034. Lowercasing before comparison prevents bypasses likeGitHub.COM. -
Two imports added, zero behavior changed for valid inputs — The
net/urlandstringspackages are added to the import block. Requests with valid GitHub URLs pass through identically; only invalid URLs are now rejected early.
Prevention & Best Practices
1. Always Validate URLs at the Entry Point
Never let a URL travel through your application layers before being validated. The fix correctly places isAllowedSubTemplateURL before the call to serverService.ApplySubTemplateFromGithub(). If validation had been pushed into the service layer, future refactors might bypass it.
2. Use Allowlists, Not Blocklists
Blocklisting known-bad hosts (e.g., 169.254.169.254) is a losing game — there are dozens of internal address ranges and metadata endpoints across cloud providers. An allowlist of exactly the hosts you intend to reach is far more robust.
3. Validate Redirect Destinations Too
The PR description notes that the CheckRedirect handler at line 630 processes redirects without validating destinations. A complete SSRF defense should also apply the same allowlist check to redirect targets:
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if !isAllowedSubTemplateURL(req.URL.String()) {
return fmt.Errorf("redirect to disallowed host: %s", req.URL.Host)
}
return nil
},
}
4. Consider Network-Level Controls as Defense-in-Depth
Even with URL validation in code, configure your server's egress firewall to block requests to RFC 1918 private ranges and cloud metadata IPs. Code-level and network-level controls complement each other.
5. Reference Standards
- OWASP SSRF Prevention Cheat Sheet: Covers allowlisting, DNS rebinding, and redirect validation in depth
- CWE-918: The authoritative definition of SSRF
- Go
net/urldocs: Useurl.Parse()+u.Hostname()as shown in this fix
Key Takeaways
- The function name
ApplySubTemplateFromGithubwas not a security control — naming a function after its intended input doesn't restrict actual input. Validation code must be explicit. url.Parse()+u.Hostname()is the correct pattern in Go for extracting a comparable hostname from a user-supplied URL string; avoid substring or regex checks on raw URL strings.- Redirect chains can bypass entry-point validation — the
CheckRedirecthandler inserver.goat line 630 is a second SSRF surface that should apply the same allowlist. - SSRF primitives are chain links, not dead ends — even if the template processing doesn't directly expose the fetched content, the outbound request itself can leak information or trigger side effects in internal services.
- Two-step SSRF via open redirectors is a real attack pattern: submitting a URL that passes a naive check but immediately redirects to an internal target is a well-documented bypass technique.
How Orbis AppSec Detected This
- Source: HTTP form parameter
f.URLbound viac.ShouldBind()in theapplySubTemplatehandler (server.go:340) - Sink:
a.serverService.ApplySubTemplateFromGithub(f.URL)— a function that issues an outbound HTTP request using the caller-supplied URL - Missing control: No hostname validation or allowlist between the form binding and the service call; the
CheckRedirecthandler also lacked destination validation - CWE: CWE-918 — Server-Side Request Forgery (SSRF)
- Fix: Introduced
isAllowedSubTemplateURL()to parse and allowlist onlygithub.comandraw.githubusercontent.combefore the service call is reached
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The applySubTemplate vulnerability is a reminder that intent is not enforcement. A function named ApplySubTemplateFromGithub still accepted any URL until explicit validation was added. The fix — a small, focused isAllowedSubTemplateURL function using Go's standard net/url package — closes the SSRF surface with minimal code and zero impact on legitimate requests.
SSRF vulnerabilities are particularly dangerous in cloud-hosted services where metadata APIs, internal databases, and service meshes are reachable from the application's network context. Proactively validating URLs at handler boundaries, enforcing strict allowlists, and extending that validation to redirect handlers are the habits that keep Go services safe.