@@ -24,12 +24,46 @@ impl TokenStore {
2424/// 1. The hostname can be parsed as part of a valid HTTPS URL
2525/// 2. The hostname doesn't contain unsafe characters (null bytes, CRLF)
2626/// 3. The hostname matches expected patterns (github.com or enterprise servers)
27+ /// 4. The hostname is not a private/internal network address (SSRF protection)
2728fn validate_oauth_hostname ( hostname : & str ) -> Result < ( ) , String > {
2829 // Check for null bytes and CRLF injection
2930 if hostname. contains ( '\0' ) || hostname. contains ( '\r' ) || hostname. contains ( '\n' ) {
3031 return Err ( "Invalid hostname: contains control characters" . to_string ( ) ) ;
3132 }
3233
34+ // Block internal/private network addresses (SSRF protection)
35+ let lower = hostname. to_lowercase ( ) ;
36+ if lower == "localhost"
37+ || lower. starts_with ( "127." )
38+ || lower. starts_with ( "10." )
39+ || lower. starts_with ( "192.168." )
40+ || lower. starts_with ( "172.16." )
41+ || lower. starts_with ( "172.17." )
42+ || lower. starts_with ( "172.18." )
43+ || lower. starts_with ( "172.19." )
44+ || lower. starts_with ( "172.20." )
45+ || lower. starts_with ( "172.21." )
46+ || lower. starts_with ( "172.22." )
47+ || lower. starts_with ( "172.23." )
48+ || lower. starts_with ( "172.24." )
49+ || lower. starts_with ( "172.25." )
50+ || lower. starts_with ( "172.26." )
51+ || lower. starts_with ( "172.27." )
52+ || lower. starts_with ( "172.28." )
53+ || lower. starts_with ( "172.29." )
54+ || lower. starts_with ( "172.30." )
55+ || lower. starts_with ( "172.31." )
56+ || lower. starts_with ( "169.254." )
57+ || lower. starts_with ( "0." )
58+ || lower. contains ( "metadata.google" )
59+ || lower. contains ( "metadata.aws" )
60+ || lower == "metadata"
61+ || lower. ends_with ( ".internal" )
62+ || lower. ends_with ( ".local" )
63+ {
64+ return Err ( "Invalid hostname: internal addresses not allowed" . to_string ( ) ) ;
65+ }
66+
3367 // Try to parse as a URL to validate the hostname format
3468 let test_url = format ! ( "https://{}/login/oauth/access_token" , hostname) ;
3569 let parsed = Url :: parse ( & test_url) . map_err ( |e| format ! ( "Invalid hostname format: {}" , e) ) ?;
@@ -150,8 +184,11 @@ async fn do_exchange_oauth_code(
150184 ( "code" , code) ,
151185 ] ;
152186
153- // Create HTTP client
154- let client = reqwest:: Client :: new ( ) ;
187+ // Create HTTP client with timeout to prevent hanging connections
188+ let client = reqwest:: Client :: builder ( )
189+ . timeout ( std:: time:: Duration :: from_secs ( 30 ) )
190+ . build ( )
191+ . map_err ( |e| format ! ( "Failed to create HTTP client: {}" , e) ) ?;
155192
156193 // Make the POST request to exchange the code for a token
157194 let response = client
0 commit comments