| 217 | * - Or both of the above |
| 218 | */ |
| 219 | export function isPermittedRedirect( |
| 220 | originalUrl: string, |
| 221 | redirectUrl: string, |
| 222 | ): boolean { |
| 223 | try { |
| 224 | const parsedOriginal = new URL(originalUrl) |
| 225 | const parsedRedirect = new URL(redirectUrl) |
| 226 | |
| 227 | if (parsedRedirect.protocol !== parsedOriginal.protocol) { |
| 228 | return false |
| 229 | } |
| 230 | |
| 231 | if (parsedRedirect.port !== parsedOriginal.port) { |
| 232 | return false |
| 233 | } |
| 234 | |
| 235 | if (parsedRedirect.username || parsedRedirect.password) { |
| 236 | return false |
| 237 | } |
| 238 | |
| 239 | // Now check hostname conditions |
| 240 | // 1. Adding www. is allowed: example.com -> www.example.com |
| 241 | // 2. Removing www. is allowed: www.example.com -> example.com |
| 242 | // 3. Same host (with or without www.) is allowed: paths can change |
| 243 | const stripWww = (hostname: string) => hostname.replace(/^www\./, '') |
| 244 | const originalHostWithoutWww = stripWww(parsedOriginal.hostname) |
| 245 | const redirectHostWithoutWww = stripWww(parsedRedirect.hostname) |
| 246 | return originalHostWithoutWww === redirectHostWithoutWww |
| 247 | } catch (_error) { |
| 248 | return false |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | /** |
| 253 | * Helper function to handle fetching URLs with custom redirect handling |