Skip to content

Comp 1145 Added Server-Side Request Forgery (SSRF) protection to the validate-creds API#962

Merged
munishchouhan merged 22 commits intomasterfrom
COMP-1145-fix-ssrf
Mar 11, 2026
Merged

Comp 1145 Added Server-Side Request Forgery (SSRF) protection to the validate-creds API#962
munishchouhan merged 22 commits intomasterfrom
COMP-1145-fix-ssrf

Conversation

@munishchouhan
Copy link
Member

@munishchouhan munishchouhan commented Jan 14, 2026

Summary

  • Add SSRF (Server-Side Request Forgery) protection to the /validate-creds and /v1alpha2/validate-creds endpoints to prevent attackers from using the credential validation flow to probe internal infrastructure or cloud
    metadata services
  • SsrfValidator resolves user-supplied registry hostnames via DNS and rejects any that resolve to private (RFC 1918), loopback, link-local, or cloud metadata IPs (169.254.169.254, 169.254.170.2, fd00:ec2::254)
  • SSRF protection is enabled by default and can be toggled via wave.security.ssrf-protection.enabled (disabled in local dev profile)

Test plan

  • Unit tests for SsrfValidator covering private IPs, loopback, localhost variations, cloud metadata IPs, null/empty inputs, and public hostnames
  • Integration tests for /v1alpha2/validate-creds endpoint verifying SSRF attempts return HTTP 400
  • Verify existing credential validation tests still pass
  • Verify SSRF protection is disabled in local development (application-local.yml)

Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
@munishchouhan munishchouhan changed the title Comp 1145 fix ssrf Comp 1145 Added Server-Side Request Forgery (SSRF) protection to the /validate-creds Jan 14, 2026
@munishchouhan munishchouhan changed the title Comp 1145 Added Server-Side Request Forgery (SSRF) protection to the /validate-creds Comp 1145 Added Server-Side Request Forgery (SSRF) protection to the validate-creds API Jan 15, 2026
munishchouhan and others added 5 commits January 15, 2026 06:58
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
@munishchouhan
Copy link
Member Author

Tested locally:

  1. invalid registry
wave %  curl -X POST http://localhost:9090/v1alpha2/validate-creds \
    -H "Content-Type: application/json" \
    -d '{
      "userName": "test",
      "password": "test",
      "registry": "localhost"
    }'

{"message":"Access to localhost is not allowed: localhost - Error ID: a8562c2af15c"}

wave % curl -X POST http://localhost:9090/v1alpha2/validate-creds \
    -H "Content-Type: application/json" \
    -d '{
      "userName": "test",
      "password": "test",
      "registry": "10.0.0.1"
    }'

{"message":"Access to private IP range is not allowed: 10.0.0.1 - Error ID: f5c9bc154a46"}

wave %  curl -X POST http://localhost:9090/v1alpha2/validate-creds \
    -H "Content-Type: application/json" \
    -d '{
      "userName": "test",
      "password": "test",
      "registry": "169.254.169.254"
    }'

{"message":"Access to cloud metadata service is not allowed: 169.254.169.254 - Error ID: 4fce98fc28b4"}

wave %  curl -X POST http://localhost:9090/v1alpha2/validate-creds \
    -H "Content-Type: application/json" \
    -d '{
      "userName": "test",
      "password": "test",
      "registry": "169.254.169.254"
    }'

{"message":"Access to cloud metadata service is not allowed: 169.254.169.254 - Error ID: 4fce98fc28b4"}
  1. valid registry
wave % curl -X POST http://localhost:9090/v1alpha2/validate-creds \
    -H "Content-Type: application/json" \
    -d '{
      "userName": "test",
      "password": "test",
      "registry": "docker.io"
    }'

false%    

@munishchouhan
Copy link
Member Author

@claude review

@claude
Copy link

claude bot commented Mar 10, 2026

Claude finished @munishchouhan's task —— View job


PR Review: SSRF Protection Implementation

Reviewing the SSRF protection implementation for the validate-creds API endpoints.

Todo List:

  • Review SSRF configuration setup
  • Analyze SsrfValidator implementation for security gaps
  • Check controller integration
  • Review test coverage and quality
  • Verify configuration management
  • Provide final recommendations

Review Summary

This is a solid SSRF protection implementation with good security coverage. The code follows defensive security principles and includes comprehensive test coverage. Here's my detailed analysis:

✅ Strengths

Security Coverage

  • Properly blocks all major SSRF attack vectors: private networks (RFC 1918), loopback, link-local, and cloud metadata services
  • Includes IPv6 protection for unique local addresses (fc00::/7)
  • Validates all resolved IPs from DNS, preventing DNS rebinding attacks
  • Fails closed on DNS resolution failures - good security practice

Implementation Quality

  • Clean separation of concerns with dedicated SsrfValidator utility class
  • Configuration-driven with proper defaults (enabled by default, disabled in local dev)
  • Uses appropriate exception handling with descriptive error messages
  • Follows existing codebase patterns and conventions

Test Coverage

  • Comprehensive unit tests covering all attack vectors
  • Integration tests verifying controller behavior
  • Tests both positive and negative cases
  • Includes edge cases like null/empty inputs

🔍 Areas for Consideration

DNS Rebinding Edge Cases

  • SsrfValidator.groovy:70 - Consider adding a timeout for DNS resolution to prevent DNS-based DoS attacks
  • The current implementation resolves all IPs returned by DNS, which is correct but could be slow for domains with many A records

Error Information Disclosure

  • SsrfValidator.groovy:76,88,92,96,100,107 - Error messages include the actual IP addresses. While helpful for debugging, consider if this provides too much information to attackers about network topology

IPv6 Completeness

  • SsrfValidator.groovy:104-109 - Good IPv6 unique local address detection, but consider also checking for IPv6 loopback (::1) explicitly in the localhost names list (though isLoopbackAddress() should catch it)

Configuration Validation

  • SSRFConfig.groovy:34 - Consider adding validation to ensure the configuration value is explicitly set rather than relying on default

🏗️ Code Quality

Controller Integration (ValidateController.groovy:55-59)

  • Clean integration with proper dependency injection
  • Consistent application to both endpoints
  • Good use of configuration flag

Test Quality (ValidateCredsControllerTest.groovy:194-264)

  • Well-structured integration tests
  • Proper error validation
  • Good coverage of realistic attack scenarios

🔒 Security Assessment

This implementation effectively mitigates:

  • ✅ Direct IP-based SSRF attacks
  • ✅ Localhost/loopback exploitation
  • ✅ Private network reconnaissance
  • ✅ Cloud metadata service access
  • ✅ DNS rebinding attacks
  • ✅ IPv6-based bypasses

The fail-closed approach on DNS errors is particularly good - better to reject legitimate but unresolvable hostnames than allow potential attacks.

📋 Recommendations

  1. Consider DNS timeout: Add a reasonable timeout (2-3 seconds) for DNS resolution to prevent DoS
  2. Review error verbosity: Evaluate if including resolved IPs in error messages is necessary for your threat model
  3. Add monitoring: Consider adding metrics/logging for blocked SSRF attempts for security monitoring

Verdict: APPROVED

This is a well-implemented SSRF protection mechanism that significantly improves the security posture of the validate-creds endpoints. The code is production-ready and follows security best practices.

@munishchouhan
Copy link
Member Author

tested locally:

1. Should be REJECTED - loopback                                                                                                                           
%curl -s -X POST http://localhost:9090/v1alpha2/validate-creds -H "Content-Type: application/json" -d '{"userName":"test","password":"test","registry":"127.0.0.1"}'

{"message":"Access to loopback address is not allowed: 127.0.0.1"}%                                                                                                                             

2. Should be REJECTED - localhost                                                                                                                          
  % curl -s -X POST http://localhost:9090/v1alpha2/validate-creds -H "Content-Type: application/json" -d '{"userName":"test","password":"test","registry":"localhost"}'

{"message":"Access to localhost is not allowed: localhost"}%                                                                                                                                    

3. Should be REJECTED - AWS metadata
  % curl -s -X POST http://localhost:9090/v1alpha2/validate-creds -H "Content-Type: application/json" -d '{"userName":"test","password":"test","registry":"169.254.169.254"}'

{"message":"Access to cloud metadata service is not allowed: 169.254.169.254"}%                                                                                                                 

4. Should be REJECTED - private IP
  % curl -s -X POST http://localhost:9090/v1alpha2/validate-creds -H "Content-Type: application/json" -d '{"userName":"test","password":"test","registry":"10.0.0.1"}'

{"message":"Access to private IP address is not allowed: 10.0.0.1"}%  

 5. Should be ALLOWED (auth will fail, but SSRF check passes)
  % curl -s -X POST http://localhost:9090/v1alpha2/validate-creds -H "Content-Type: application/json" -d '{"userName":"test","password":"test","registry":"docker.io"}'

false%   

@munishchouhan
Copy link
Member Author

@pditommaso I have simplified and tested this PR
Please review

Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
Copy link
Collaborator

@pditommaso pditommaso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good security fix overall. The core SSRF protection logic is solid — DNS resolution + IP validation before the auth call is the right approach. A few things to address:


extractHostname — missing port stripping

The registry field can contain a port (e.g. 192.168.1.1:5000). When no scheme is present, extractHostname returns the string as-is, so InetAddress.getAllByName("192.168.1.1:5000") fails with UnknownHostException — giving a confusing "Unable to resolve host" error instead of a proper SSRF rejection.

Should strip the port for bare host:port inputs:

// Strip port from host:port
int colonIdx = host.lastIndexOf(':')
if (colonIdx > 0) {
    return host.substring(0, colonIdx)
}

validateIpAddress — error messages expose internal info

The error messages expose resolved IPs to the caller (e.g. "Access to private IP address is not allowed: 10.0.0.1"). This confirms to an attacker that DNS resolved to a specific internal address. Consider using a generic message like "Invalid registry hostname" in the exception, and logging the detailed reason server-side.


METADATA_IPS — other cloud providers

This only covers AWS metadata endpoints. For completeness you may want to add other cloud providers, e.g. 100.100.100.200 (Alibaba Cloud IMDS). GCP and Azure use 169.254.169.254 which is already covered by the link-local check, so those are fine.

Signed-off-by: munishchouhan <hrma017@gmail.com>
Signed-off-by: munishchouhan <hrma017@gmail.com>
@munishchouhan
Copy link
Member Author

Good security fix overall. The core SSRF protection logic is solid — DNS resolution + IP validation before the auth call is the right approach. A few things to address:

extractHostname — missing port stripping

The registry field can contain a port (e.g. 192.168.1.1:5000). When no scheme is present, extractHostname returns the string as-is, so InetAddress.getAllByName("192.168.1.1:5000") fails with UnknownHostException — giving a confusing "Unable to resolve host" error instead of a proper SSRF rejection.

Should strip the port for bare host:port inputs:

// Strip port from host:port
int colonIdx = host.lastIndexOf(':')
if (colonIdx > 0) {
    return host.substring(0, colonIdx)
}

validateIpAddress — error messages expose internal info

The error messages expose resolved IPs to the caller (e.g. "Access to private IP address is not allowed: 10.0.0.1"). This confirms to an attacker that DNS resolved to a specific internal address. Consider using a generic message like "Invalid registry hostname" in the exception, and logging the detailed reason server-side.

METADATA_IPS — other cloud providers

This only covers AWS metadata endpoints. For completeness you may want to add other cloud providers, e.g. 100.100.100.200 (Alibaba Cloud IMDS). GCP and Azure use 169.254.169.254 which is already covered by the link-local check, so those are fine.

Fixed

…trollerTest.groovy

Co-authored-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
@munishchouhan munishchouhan merged commit b5969fa into master Mar 11, 2026
3 checks passed
@munishchouhan munishchouhan deleted the COMP-1145-fix-ssrf branch March 11, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants