Skip to content

Commit f548620

Browse files
committed
security: add SSRF protection for gateway route target URLs
1 parent 1dd04e6 commit f548620

1 file changed

Lines changed: 54 additions & 0 deletions

File tree

internal/core/services/gateway.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ func (s *GatewayService) CreateRoute(ctx context.Context, params ports.CreateRou
8080
paramNames = matcher.ParamNames
8181
}
8282

83+
// Validate target URL to prevent SSRF attacks
84+
if err := isAllowedTarget(params.Target); err != nil {
85+
return nil, fmt.Errorf("invalid target: %w", err)
86+
}
87+
8388
route := &domain.GatewayRoute{
8489
ID: uuid.New(),
8590
UserID: userID,
@@ -620,3 +625,52 @@ func (rt *retryTransport) jitter(max time.Duration) time.Duration {
620625
frac := val / float64(math.MaxUint64)
621626
return time.Duration(float64(max) * frac)
622627
}
628+
629+
// isAllowedTarget validates that a target URL doesn't point to internal/private networks.
630+
// This prevents SSRF attacks where an attacker could route requests to cloud metadata
631+
// endpoints (169.254.169.254), localhost, or private IP ranges.
632+
func isAllowedTarget(targetURL string) error {
633+
u, err := url.Parse(targetURL)
634+
if err != nil {
635+
return fmt.Errorf("invalid target URL: %w", err)
636+
}
637+
638+
host := u.Hostname()
639+
ip := net.ParseIP(host)
640+
641+
// Check for localhost
642+
if host == "localhost" || host == "127.0.0.1" {
643+
return fmt.Errorf("localhost targets not allowed")
644+
}
645+
646+
// Check for loopback IP
647+
if ip != nil && ip.IsLoopback() {
648+
return fmt.Errorf("loopback targets not allowed")
649+
}
650+
651+
// Check for link-local (169.254.x.x - Azure/AWS metadata)
652+
if ip != nil && ip.IsLinkLocalUnicast() {
653+
return fmt.Errorf("link-local addresses not allowed")
654+
}
655+
656+
// Check for private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
657+
if ip != nil && ip.IsPrivate() {
658+
return fmt.Errorf("private IP targets not allowed")
659+
}
660+
661+
// Check for reserved metadata addresses (169.254.0.0/16)
662+
if ip != nil && isReservedIP(ip) {
663+
return fmt.Errorf("reserved IP targets not allowed")
664+
}
665+
666+
return nil
667+
}
668+
669+
// isReservedIP checks for IP addresses used by cloud metadata services.
670+
func isReservedIP(ip net.IP) bool {
671+
// 169.254.0.0/16 - Azure/AWS/gcp metadata endpoints
672+
if len(ip) >= 2 && ip[0] == 169 && ip[1] == 254 {
673+
return true
674+
}
675+
return false
676+
}

0 commit comments

Comments
 (0)