Dns Rebinding in Buffalo with Basic Auth
Dns Rebinding in Buffalo with Basic Auth — how this specific combination creates or exposes the vulnerability
Buffalo is a convention-over-configuration web framework for Go. When Basic Authentication is enforced at the application layer (for example via a before filter that checks an Authorization header), DNS Rebinding can bypass those checks in certain network setups, particularly when the app trusts the Host header or relies on hostname-based routing for access control.
In a typical Buffalo app, you might protect admin routes with a before action that validates credentials:
package actions
import (
"github.com/gobuffalo/buffalo"
"net/http"
)
func RequireAuth(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
user, pass, ok := c.Request().BasicAuth()
if !ok || !checkCredentials(user, pass) {
c.Response().Header().Set("WWW-Authenticate", `Basic realm="restricted"`)
return c.Error(http.StatusUnauthorized)
}
return next(c)
}
}
The vulnerability arises when DNS Rebinding is combined with assumptions about the Host header. An attacker registers a domain (example.com) and controls DNS to rapidly switch the resolved IP between an internal target (e.g., 127.0.0.1) and an external attacker-controlled server. If the Buffalo application uses the Host header to decide whether to apply Basic Auth (e.g., allowing local access for development or skipping auth for internal addresses), the rebinding can cause the app to believe a request originated from a trusted source, while the attacker’s remote server handles the request after the IP change.
Consider this simplified check:
func beforeApp(c buffalo.Context) error {
host := c.Request().Host // vulnerable: using Host header for access decisions
if host == "localhost" || host == "127.0.0.1" {
// Skip Basic Auth for local development
return nil
}
return RequireAuth(c)
}
During a rebinding scenario, the initial request to example.com may resolve to 127.0.0.1, causing the app to skip authentication. The subsequent request may be routed to the attacker’s server, but if the app caches or trusts the hostname-based decision, unauthorized access can persist. Even if the app does not skip auth, a misconfigured reverse proxy or load balancer that forwards the original Host header can cause Buffalo to trust a hostname that no longer maps to a benign internal service, allowing authenticated sessions to be hijacked or sensitive endpoints to be exposed.
Another realistic scenario involves HTTP client calls made by Buffalo actions (e.g., fetching configuration from an internal service). If those calls use the Host header from the incoming request without validating the resolved IP, an attacker can induce the app to send credentials or session tokens to a malicious endpoint. This maps to common OWASP API Top 10 items such as Broken Object Level Authorization (BOLA) when access control decisions are based on a mutable network property rather than verified identity, and it can also intersect with Server-Side Request Forgery (SSRF) if the app follows attacker-controlled redirects or hosts.
Basic Auth-Specific Remediation in Buffalo — concrete code fixes
Remediation focuses on removing any reliance on the Host header for security decisions and ensuring credentials are validated consistently for every request. Never skip authentication based on perceived network locality.
1. Remove Host-based access logic entirely. Always apply Basic Auth (or session checks) uniformly:
func RequireAuth(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
user, pass, ok := c.Request().BasicAuth()
if !ok || !checkCredentials(user, pass) {
c.Response().Header().Set("WWW-Authenticate", `Basic realm="restricted"`)
return c.Error(http.StatusUnauthorized)
}
// Safe to proceed
return next(c)
}
}
// Apply globally in bootstrap.go
app := buffalo.New(buffalo.Options{
// ... other options
})
app.Use(RequireAuth)
2. If you must allow unauthenticated access for specific routes (e.g., public health checks), be explicit and avoid hostname checks:
func PublicHealthCheck(c buffalo.Context) error {
// No auth required for this endpoint; ensure it does not leak sensitive data
return c.Render(200, r.JSON(map[string]string{"status": "ok"}))
}
// Register without RequireAuth
app.GET("/health", PublicHealthCheck)
3. When making outbound HTTP requests from Buffalo actions, do not forward the incoming Host header. Instead, use a fixed, validated host or service identifier, and enforce strict URL validation to prevent SSRF:
import (
"net/http"
"net/url"
)
func fetchInternalConfig() (string, error) {
target := "https://config.internal.example.com/settings"
parsed, err := url.Parse(target)
if err != nil {
return "", err
}
req, err := http.NewRequest("GET", parsed.String(), nil)
if err != nil {
return "", err
}
// Do not set Host from user input; use the parsed host
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// process resp.Body
return "ok", nil
}
4. Use middleware that validates the request’s server name via the TLS ServerName or a strict allow-list of hostnames, rather than the raw Host header:
func SecureHost(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
const allowedHost = "api.example.com"
if c.Request().Host != allowedHost {
return c.Error(http.StatusBadRequest)
}
return next(c)
}
}
These changes ensure that DNS Rebinding cannot manipulate access control by altering the perceived source host. Authentication is applied consistently, and outbound calls do not reflect attacker-controlled input.