mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2026-10-01 12:11:19 +02:00
* 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>
157 lines
5.1 KiB
Go
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
|
|
}
|