Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
168
cloud/maplepress-backend/pkg/security/clientip/extractor.go
Normal file
168
cloud/maplepress-backend/pkg/security/clientip/extractor.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package clientip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Extractor provides secure client IP address extraction
|
||||
// CWE-348: Prevents X-Forwarded-For header spoofing by validating trusted proxies
|
||||
type Extractor struct {
|
||||
trustedProxies []*net.IPNet
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewExtractor creates a new IP extractor with trusted proxy configuration
|
||||
// trustedProxyCIDRs should contain CIDR blocks of trusted reverse proxies
|
||||
// Example: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
|
||||
func NewExtractor(trustedProxyCIDRs []string, logger *zap.Logger) (*Extractor, error) {
|
||||
var trustedProxies []*net.IPNet
|
||||
|
||||
for _, cidr := range trustedProxyCIDRs {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
logger.Error("failed to parse trusted proxy CIDR",
|
||||
zap.String("cidr", cidr),
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
trustedProxies = append(trustedProxies, ipNet)
|
||||
}
|
||||
|
||||
logger.Info("client IP extractor initialized",
|
||||
zap.Int("trusted_proxy_ranges", len(trustedProxies)))
|
||||
|
||||
return &Extractor{
|
||||
trustedProxies: trustedProxies,
|
||||
logger: logger.Named("client-ip-extractor"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewDefaultExtractor creates an extractor with no trusted proxies
|
||||
// This is safe for direct connections but will ignore X-Forwarded-For headers
|
||||
func NewDefaultExtractor(logger *zap.Logger) *Extractor {
|
||||
logger.Warn("client IP extractor initialized with NO trusted proxies - X-Forwarded-For will be ignored")
|
||||
return &Extractor{
|
||||
trustedProxies: []*net.IPNet{},
|
||||
logger: logger.Named("client-ip-extractor"),
|
||||
}
|
||||
}
|
||||
|
||||
// Extract extracts the real client IP address from the HTTP request
|
||||
// CWE-348: Secure implementation that prevents header spoofing
|
||||
func (e *Extractor) Extract(r *http.Request) string {
|
||||
// Step 1: Get the immediate connection's remote address
|
||||
remoteAddr := r.RemoteAddr
|
||||
|
||||
// Remove port from RemoteAddr (format: "IP:port" or "[IPv6]:port")
|
||||
remoteIP := e.stripPort(remoteAddr)
|
||||
|
||||
// Step 2: Parse the remote IP
|
||||
parsedRemoteIP := net.ParseIP(remoteIP)
|
||||
if parsedRemoteIP == nil {
|
||||
e.logger.Warn("failed to parse remote IP address",
|
||||
zap.String("remote_addr", remoteAddr))
|
||||
return remoteIP // Return as-is if we can't parse it
|
||||
}
|
||||
|
||||
// Step 3: Check if the immediate connection is from a trusted proxy
|
||||
if !e.isTrustedProxy(parsedRemoteIP) {
|
||||
// NOT from a trusted proxy - do NOT trust X-Forwarded-For header
|
||||
// This prevents clients from spoofing their IP by setting the header
|
||||
e.logger.Debug("remote IP is not a trusted proxy, using RemoteAddr",
|
||||
zap.String("remote_ip", remoteIP))
|
||||
return remoteIP
|
||||
}
|
||||
|
||||
// Step 4: Remote IP is trusted, check X-Forwarded-For header
|
||||
// Format: "client, proxy1, proxy2" (leftmost is original client)
|
||||
xff := r.Header.Get("X-Forwarded-For")
|
||||
if xff == "" {
|
||||
// No X-Forwarded-For header, use RemoteAddr
|
||||
e.logger.Debug("no X-Forwarded-For header from trusted proxy",
|
||||
zap.String("remote_ip", remoteIP))
|
||||
return remoteIP
|
||||
}
|
||||
|
||||
// Step 5: Parse X-Forwarded-For header
|
||||
// Take the FIRST IP (leftmost) which should be the original client
|
||||
ips := strings.Split(xff, ",")
|
||||
if len(ips) == 0 {
|
||||
e.logger.Debug("empty X-Forwarded-For header",
|
||||
zap.String("remote_ip", remoteIP))
|
||||
return remoteIP
|
||||
}
|
||||
|
||||
// Get the first IP and trim whitespace
|
||||
clientIP := strings.TrimSpace(ips[0])
|
||||
|
||||
// Step 6: Validate the client IP
|
||||
parsedClientIP := net.ParseIP(clientIP)
|
||||
if parsedClientIP == nil {
|
||||
e.logger.Warn("invalid IP in X-Forwarded-For header",
|
||||
zap.String("xff", xff),
|
||||
zap.String("client_ip", clientIP))
|
||||
return remoteIP // Fall back to RemoteAddr
|
||||
}
|
||||
|
||||
e.logger.Debug("extracted client IP from X-Forwarded-For",
|
||||
zap.String("client_ip", clientIP),
|
||||
zap.String("remote_proxy", remoteIP),
|
||||
zap.String("xff_chain", xff))
|
||||
|
||||
return clientIP
|
||||
}
|
||||
|
||||
// ExtractOrDefault extracts the client IP or returns a default value
|
||||
func (e *Extractor) ExtractOrDefault(r *http.Request, defaultIP string) string {
|
||||
ip := e.Extract(r)
|
||||
if ip == "" {
|
||||
return defaultIP
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
// isTrustedProxy checks if an IP is in the trusted proxy list
|
||||
func (e *Extractor) isTrustedProxy(ip net.IP) bool {
|
||||
for _, ipNet := range e.trustedProxies {
|
||||
if ipNet.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// stripPort removes the port from an address string
|
||||
// Handles both IPv4 (192.168.1.1:8080) and IPv6 ([::1]:8080) formats
|
||||
func (e *Extractor) stripPort(addr string) string {
|
||||
// For IPv6, check for bracket format [IP]:port
|
||||
if strings.HasPrefix(addr, "[") {
|
||||
// IPv6 format: [::1]:8080
|
||||
if idx := strings.LastIndex(addr, "]:"); idx != -1 {
|
||||
return addr[1:idx] // Extract IP between [ and ]
|
||||
}
|
||||
// Malformed IPv6 address
|
||||
return addr
|
||||
}
|
||||
|
||||
// For IPv4, split on last colon
|
||||
if idx := strings.LastIndex(addr, ":"); idx != -1 {
|
||||
return addr[:idx]
|
||||
}
|
||||
|
||||
// No port found
|
||||
return addr
|
||||
}
|
||||
|
||||
// GetTrustedProxyCount returns the number of configured trusted proxy ranges
|
||||
func (e *Extractor) GetTrustedProxyCount() int {
|
||||
return len(e.trustedProxies)
|
||||
}
|
||||
|
||||
// HasTrustedProxies returns true if any trusted proxies are configured
|
||||
func (e *Extractor) HasTrustedProxies() bool {
|
||||
return len(e.trustedProxies) > 0
|
||||
}
|
||||
19
cloud/maplepress-backend/pkg/security/clientip/provider.go
Normal file
19
cloud/maplepress-backend/pkg/security/clientip/provider.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package clientip
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
|
||||
)
|
||||
|
||||
// ProvideExtractor provides a client IP extractor configured from the application config
|
||||
func ProvideExtractor(cfg *config.Config, logger *zap.Logger) (*Extractor, error) {
|
||||
// If no trusted proxies configured, use default (no X-Forwarded-For trust)
|
||||
if len(cfg.Security.TrustedProxies) == 0 {
|
||||
logger.Info("no trusted proxies configured - X-Forwarded-For headers will be ignored for security")
|
||||
return NewDefaultExtractor(logger), nil
|
||||
}
|
||||
|
||||
// Create extractor with trusted proxies
|
||||
return NewExtractor(cfg.Security.TrustedProxies, logger)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue