Files
oauth2-proxy/pkg/ip/realclientip.go
bea3f04bf8 release: v7.15.5 (#3553)
* update to release version v7.15.5

* Merge commit from fork

Signed-off-by: Jan Larwig <jan@larwig.com>
Co-authored-by: blakeroberts-wk <blake.roberts@workiva.com>

* Merge commit from fork

Signed-off-by: Jan Larwig <jan@larwig.com>

* Merge commit from fork

* fix: validate trusted IP proxy headers

Respect trusted proxy boundaries before using real-client-IP headers for authentication bypass decisions and safely traverse X-Forwarded-For chains.

Signed-off-by: Jan Larwig <jan@larwig.com>

* fix: trusted-ip header bypass

Signed-off-by: Jan Larwig <jan@larwig.com>

* docs: add changelog entry

Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz>

---------

Signed-off-by: Jan Larwig <jan@larwig.com>
Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz>

* docs: changelog for v7.15.5

Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz>

* docs: update order of owners for prow

Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz>

* ci: make the linter happy again

Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz>

---------

Signed-off-by: Jan Larwig <jan@larwig.com>
Signed-off-by: Jan Larwig <jan.larwig@digits.schwarz>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jan Larwig <jan@larwig.com>
Co-authored-by: blakeroberts-wk <blake.roberts@workiva.com>
Co-authored-by: Jan Larwig <jan.larwig@digits.schwarz>
2026-10-01 11:01:25 +02:00

157 lines
5.1 KiB
Go

package ip
import (
"fmt"
"net"
"net/http"
"strings"
ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
)
func GetRealClientIPParser(headerKey string) (ipapi.RealClientIPParser, error) {
headerKey = http.CanonicalHeaderKey(headerKey)
switch headerKey {
case http.CanonicalHeaderKey("X-Forwarded-For"),
http.CanonicalHeaderKey("X-Real-IP"),
http.CanonicalHeaderKey("X-ProxyUser-IP"),
http.CanonicalHeaderKey("X-Envoy-External-Address"),
// Cloudflare specific Real-IP header
http.CanonicalHeaderKey("CF-Connecting-IP"):
return &xForwardedForClientIPParser{header: headerKey}, nil
}
// TODO: implement the more standardized but more complex `Forwarded` header.
return nil, fmt.Errorf("the http header key (%s) is either invalid or unsupported", headerKey)
}
type xForwardedForClientIPParser struct {
header string
}
// GetRealClientIP obtain the IP address of the end-user (not proxy).
// Parses headers sharing the format as specified by:
// * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For.
// Returns the `<client>` portion specified in the above document.
// Additionally, is capable of parsing IPs with the port included, for v4 in the format "<ip>:<port>" and for v6 in the
// format "[<ip>]:<port>". With-port and without-port formats are seamlessly supported concurrently.
func (p xForwardedForClientIPParser) GetRealClientIP(h http.Header) (net.IP, error) {
var ipStr string
if realIP := h.Get(p.header); realIP != "" {
ipStr = realIP
} else {
return nil, nil
}
// Each successive proxy may append itself, comma separated, to the end of the X-Forwarded-for header.
// Select only the first IP listed, as it is the client IP recorded by the first proxy.
if commaIndex := strings.IndexRune(ipStr, ','); commaIndex != -1 {
ipStr = ipStr[:commaIndex]
}
return parseClientIP(ipStr, p.header)
}
// GetClientIPFromTrustedProxy obtains the client IP from a header supplied by
// a caller that has already been verified as a trusted proxy.
func GetClientIPFromTrustedProxy(p ipapi.RealClientIPParser, req *http.Request, trustedProxies *NetSet) (net.IP, error) {
if p == nil {
return nil, fmt.Errorf("real client IP parser is required")
}
xffParser, ok := p.(*xForwardedForClientIPParser)
if !ok || xffParser.header != http.CanonicalHeaderKey("X-Forwarded-For") {
return p.GetRealClientIP(req.Header)
}
if trustedProxies == nil {
return nil, fmt.Errorf("trusted proxy list is required to parse X-Forwarded-For")
}
xff := strings.Join(req.Header.Values(xffParser.header), ",")
if xff == "" {
return nil, nil
}
chain := strings.Split(xff, ",")
for i := len(chain) - 1; i >= 0; i-- {
clientIP, err := parseClientIP(chain[i], xffParser.header)
if err != nil {
return nil, err
}
if i == 0 || !trustedProxies.Has(clientIP) {
return clientIP, nil
}
}
return nil, nil
}
func parseClientIP(ipStr string, header string) (net.IP, error) {
ipStr = strings.TrimSpace(ipStr)
if ipHost, _, err := net.SplitHostPort(ipStr); err == nil {
ipStr = ipHost
}
clientIP := net.ParseIP(ipStr)
if clientIP == nil {
return nil, fmt.Errorf("unable to parse ip (%s) from %s header", ipStr, http.CanonicalHeaderKey(header))
}
return clientIP, nil
}
// GetClientIP obtains the perceived end-user IP address from headers if p != nil else from req.RemoteAddr.
func GetClientIP(p ipapi.RealClientIPParser, req *http.Request) (net.IP, error) {
if p != nil {
return p.GetRealClientIP(req.Header)
}
return getRemoteIP(req)
}
// getRemoteIP obtains the IP of the low-level connected network host
func getRemoteIP(req *http.Request) (net.IP, error) {
// Unix domain sockets set RemoteAddr to "@" which has no meaningful IP.
// https://github.com/golang/go/blob/0fa53e41f122b1661d0678a6d36d71b7b5ad031d/src/syscall/syscall_linux.go#L506-L511
if req.RemoteAddr == "@" {
return nil, nil
}
//revive:disable:indent-error-flow
if ipStr, _, err := net.SplitHostPort(req.RemoteAddr); err != nil {
return nil, fmt.Errorf("unable to get ip and port from http.RemoteAddr (%s)", req.RemoteAddr)
} else if ip := net.ParseIP(ipStr); ip != nil {
return ip, nil
} else {
return nil, fmt.Errorf("unable to parse ip (%s)", ipStr)
}
//revive:enable:indent-error-flow
}
// GetClientString obtains the human readable string of the remote IP and optionally
// the real client IP. Callers must pass a nil parser unless the peer is trusted.
// Missing or invalid client headers leave only the transport IP in the output.
func GetClientString(p ipapi.RealClientIPParser, req *http.Request, trustedProxies *NetSet, full bool) (s string) {
var realClientIPStr string
if p != nil {
if realClientIP, err := GetClientIPFromTrustedProxy(p, req, trustedProxies); err == nil && realClientIP != nil {
realClientIPStr = realClientIP.String()
}
}
var remoteIPStr string
if remoteIP, err := getRemoteIP(req); err == nil && remoteIP != nil {
remoteIPStr = remoteIP.String()
}
if !full && realClientIPStr != "" {
return realClientIPStr
}
if full && realClientIPStr != "" {
return fmt.Sprintf("%s (%s)", remoteIPStr, realClientIPStr)
}
return remoteIPStr
}