Back to Blog
high SEVERITY7 min read

How Server-Side Request Forgery (SSRF) happens in Go HTTP handlers and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict

O
By Orbis AppSec
Published September 4, 2026Reviewed September 4, 2026

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in a Go HTTP handler (`internal/web/controller/server.go`, line 340). The `applySubTemplate` endpoint bound a user-supplied URL directly to `serverService.ApplySubTemplateFromGithub(f.URL)` without validating the host, allowing attackers to redirect the server's HTTP client to internal resources. The fix adds an `isAllowedSubTemplateURL()` function that parses the URL and enforces a strict hostname allowlist (`github.com` and `raw.githubusercontent.com`), rejecting all other destinations before the service call is made.

Vulnerability at a Glance

cweCWE-918
fixAdded isAllowedSubTemplateURL() allowlist function restricting hosts to github.com and raw.githubusercontent.com
riskAttacker forces server to make HTTP requests to internal or unintended external hosts
languageGo
root causeUser-supplied URL passed to HTTP client without hostname validation in applySubTemplate handler
vulnerabilityServer-Side Request Forgery (SSRF)

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:

  1. Attacker hosts https://attacker.com/redirect which returns HTTP 302 → http://10.0.0.1/admin
  2. Attacker submits url=https://attacker.com/redirect to the endpoint
  3. The CheckRedirect handler follows the redirect to the internal admin panel
  4. 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:

  1. Proper URL parsing via url.Parse() — Rather than substring matching (which can be bypassed with https://evil.com/github.com/path), the fix uses Go's standard net/url package to parse the URL and extract only the hostname component.

  2. Scheme enforcement — The function explicitly rejects anything that isn't http or https. This blocks file://, ftp://, gopher://, and other schemes that could be abused in certain HTTP client configurations.

  3. u.Hostname() not u.HostHostname() strips the port number before comparison, so github.com:443 and github.com both resolve to github.com. Using u.Host directly would allow github.com.evil.com to be disguised as a port specification in some edge cases.

  4. Case normalization with strings.ToLower() — Hostnames are case-insensitive per RFC 1034. Lowercasing before comparison prevents bypasses like GitHub.COM.

  5. Two imports added, zero behavior changed for valid inputs — The net/url and strings packages 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/url docs: Use url.Parse() + u.Hostname() as shown in this fix

Key Takeaways

  • The function name ApplySubTemplateFromGithub was 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 CheckRedirect handler in server.go at 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.URL bound via c.ShouldBind() in the applySubTemplate handler (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 CheckRedirect handler also lacked destination validation
  • CWE: CWE-918 — Server-Side Request Forgery (SSRF)
  • Fix: Introduced isAllowedSubTemplateURL() to parse and allowlist only github.com and raw.githubusercontent.com before 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.


References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker tricks a server into making HTTP requests to unintended destinations — including internal services, cloud metadata APIs, or other restricted hosts — by supplying a malicious URL as input.

How do you prevent SSRF in Go?

Use Go's `net/url` package to parse and validate user-supplied URLs before use. Enforce a strict hostname allowlist, reject non-HTTP(S) schemes, and avoid following redirects to unvalidated destinations.

What CWE is Server-Side Request Forgery?

SSRF is classified as CWE-918: Server-Side Request Forgery.

Is input sanitization alone enough to prevent SSRF?

No. String sanitization (e.g., checking for "github.com" as a substring) is bypassable via subdomain tricks like `evil.com/github.com`. Proper URL parsing with `url.Parse()` and strict hostname comparison is required.

Can static analysis detect SSRF in Go?

Yes. Tools like Semgrep, CodeQL, and AI-assisted scanners like Orbis AppSec can trace tainted user input from HTTP form bindings to HTTP client call sites and flag missing host validation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #196

Related Articles

critical

How SSRF via Vulnerable Dependency Versions Happens in Node.js and How to Fix It

A permissive semver range in `package.json` allowed npm to install axios versions vulnerable to SSRF (CVE-2024-39338). By bumping the minimum version from `^1.6.0` to `^1.7.4`, all downstream consumers of this SDK are now protected from server-side request forgery attacks. This critical fix required changing just one line in the dependency manifest.

critical

How Server-Side Request Forgery (SSRF) happens in JavaScript and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in playground.html where the `__forEachRdfMessageChunkFromUrl` function fetched user-controlled URLs without validating against private IP ranges or internal network addresses. The fix introduces a comprehensive `__isBlockedFetchUrl` validation function that blocks requests to localhost, private IP ranges, and link-local addresses before any fetch occurs.

critical

How Server-Side Request Forgery happens in Python FastAPI and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in app.py where the `/parse` and `/parse-video` endpoints accepted user-supplied URLs with only substring validation. The application checked if 'doubao.com' appeared anywhere in the URL string, allowing attackers to bypass this check and access internal services, cloud metadata endpoints, or scan the internal network. The fix implemented proper hostname parsing with an allowlist of legitimate domains.

critical

How Server-Side Request Forgery happens in Node.js maintenance scripts and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `maintenance/getImages.js`, where the `getImage()` function passed database-sourced URLs directly to `axios.get()` without any validation. An attacker who could modify the elements database could redirect these requests to internal network resources — including AWS cloud metadata endpoints — potentially exposing IAM credentials and other sensitive infrastructure data. The fix introduces a strict URL allowlist that limi

high

How SSRF via inconsistent IP address parsing happens in Node.js dependencies and how to fix it

A high-severity flaw (CVE-2026-69192) in the widely-used `ip-address` npm package meant that IP strings could be parsed inconsistently compared to the OS resolver and Node's own networking stack — letting an attacker slip a private/loopback address past an allowlist that used `Address4`/`Address6` for validation. This PR pins and upgrades `ip-address` from `10.1.0` to `10.3.1` in both `package.json` (via `overrides`) and `package-lock.json`, eliminating the parser divergence across the whole dep

high

How Denial of Service Attacks Happen in PHP Markdown Parsers and How to Fix Them

The league/commonmark library contained a denial of service vulnerability in its Attributes extension that could be triggered by specially crafted markdown with distinctly-named attributes. This vulnerability was fixed in version 2.10.0 by addressing how attribute names are processed during markdown parsing, preventing attackers from exhausting server resources.