Description
A regular expression used to allow-list URLs or hostnames can accept an attacker-controlled domain if it lacks anchors, leaves dots unescaped, or fails to constrain the boundary after the allowed hostname.
Potential impact
- Bypass of SSRF or open redirect validation.
- Access to external domains that are not allowed.
- Bypass of security policies.
Remediation
- Parse the URL with
URLComponentsinstead of validating the entire URL string with a regular expression. - Compare the scheme and hostname exactly. Require no user information and either no explicit port or an explicitly allowed port.
- If a regular expression is necessary for a hostname component, use start and end anchors and escape the domain separator
.as\..
Examples
Before
swift
let regex = try Regex(#"https?://www\.example\.com"#)
After
swift
import Foundation
func isAllowedURL(_ value: String) -> Bool {
guard let components = URLComponents(string: value),
components.scheme?.lowercased() == "https",
components.host?.lowercased() == "www.example.com",
components.user == nil,
components.password == nil,
components.port == nil || components.port == 443 else {
return false
}
return true
}
Explanation:
- Before: If a partial match or search result determines whether the URL is allowed, the allowed domain can appear in the middle of the URL and still pass.
- After: The example parses the URL and separately checks HTTPS, the exact hostname, the absence of user information, and the default port or port 443. Suffix domains, user information confusion, and unexpected ports cannot bypass these checks.