Skip to content

Commit 3af2a5e

Browse files
committed
fix(security): add HTTP timeout and SSRF protection to OAuth
- Add 30-second timeout to OAuth HTTP client to prevent hanging connections - Block internal network addresses (localhost, private IPs, cloud metadata endpoints) - Block .internal and .local TLDs
1 parent 0814f53 commit 3af2a5e

File tree

1 file changed

+39
-2
lines changed

1 file changed

+39
-2
lines changed

src-tauri/src/commands/auth.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
2728
fn 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

Comments
 (0)