Skip to content

Commit

Permalink
Use r.RemoteAddr to check /metrics endpoint network access
Browse files Browse the repository at this point in the history
HTTP headers like X-Forwarded-For or X-Real-Ip can be easily spoofed. As
such, it cannot be used to test if the client IP is allowed.

The recommendation is to use HTTP Basic authentication to protect the
metrics endpoint, or run Miniflux behind a trusted reverse-proxy.
  • Loading branch information
fguillot committed Mar 12, 2023
1 parent 877dbed commit b46b5df
Show file tree
Hide file tree
Showing 2 changed files with 19 additions and 13 deletions.
23 changes: 14 additions & 9 deletions http/request/client_ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,7 @@ import (
"strings"
)

func dropIPv6zone(address string) string {
i := strings.IndexByte(address, '%')
if i != -1 {
address = address[:i]
}
return address
}

// FindClientIP returns client real IP address.
// FindClientIP returns the client real IP address based on trusted Reverse-Proxy HTTP headers.
func FindClientIP(r *http.Request) string {
headers := []string{"X-Forwarded-For", "X-Real-Ip"}
for _, header := range headers {
Expand All @@ -36,6 +28,11 @@ func FindClientIP(r *http.Request) string {
}

// Fallback to TCP/IP source IP address.
return FindRemoteIP(r)
}

// FindRemoteIP returns remote client IP address.
func FindRemoteIP(r *http.Request) string {
remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
remoteIP = r.RemoteAddr
Expand All @@ -49,3 +46,11 @@ func FindClientIP(r *http.Request) string {

return remoteIP
}

func dropIPv6zone(address string) string {
i := strings.IndexByte(address, '%')
if i != -1 {
address = address[:i]
}
return address
}
9 changes: 5 additions & 4 deletions service/httpd/httpd.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router {

// Returns a 404 if the client is not authorized to access the metrics endpoint.
if route.GetName() == "metrics" && !isAllowedToAccessMetricsEndpoint(r) {
logger.Error(`[Metrics] Client not allowed: %s`, request.ClientIP(r))
logger.Error(`[Metrics] [ClientIP=%s] Client not allowed (%s)`, request.ClientIP(r), r.RemoteAddr)
http.NotFound(w, r)
return
}
Expand All @@ -222,9 +222,8 @@ func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router {
}

func isAllowedToAccessMetricsEndpoint(r *http.Request) bool {
clientIP := request.ClientIP(r)

if config.Opts.MetricsUsername() != "" && config.Opts.MetricsPassword() != "" {
clientIP := request.ClientIP(r)
username, password, authOK := r.BasicAuth()
if !authOK {
logger.Info("[Metrics] [ClientIP=%s] No authentication header sent", clientIP)
Expand All @@ -248,7 +247,9 @@ func isAllowedToAccessMetricsEndpoint(r *http.Request) bool {
logger.Fatal(`[Metrics] Unable to parse CIDR %v`, err)
}

if network.Contains(net.ParseIP(clientIP)) {
// We use r.RemoteAddr in this case because HTTP headers like X-Forwarded-For can be easily spoofed.
// The recommendation is to use HTTP Basic authentication.
if network.Contains(net.ParseIP(request.FindRemoteIP(r))) {
return true
}
}
Expand Down

0 comments on commit b46b5df

Please sign in to comment.