diff --git a/electron/services/BrowserService.ts b/electron/services/BrowserService.ts index 099b278e..c83d3383 100644 --- a/electron/services/BrowserService.ts +++ b/electron/services/BrowserService.ts @@ -21,6 +21,9 @@ function isBlockedBrowserHost(hostname: string): boolean { if (normalized === '::1' || normalized === '[::1]') return false if (normalized === '0.0.0.0' || normalized === '::') return true if (normalized === 'metadata.google.internal') return true + // Azure Instance Metadata Service — a fixed public-looking IP that resolves to + // host-only metadata (credentials). Not covered by the RFC1918 ranges below. + if (normalized === '168.63.129.16') return true if (normalized.endsWith('.local') || normalized.endsWith('.internal')) return true if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(normalized)) return false diff --git a/test/services/BrowserService.test.ts b/test/services/BrowserService.test.ts index 4ae3528a..9df4fcac 100644 --- a/test/services/BrowserService.test.ts +++ b/test/services/BrowserService.test.ts @@ -24,6 +24,14 @@ describe('BrowserService.navigate', () => { await expect(navigate('http://169.254.169.254/latest/meta-data')).rejects.toThrow(/blocked/i) }) + it('rejects cloud instance metadata endpoints', async () => { + // AWS/GCP IMDS link-local, GCP metadata hostname, and the Azure IMDS IP + // (a fixed public-looking address that the RFC1918 ranges do not catch). + await expect(navigate('http://169.254.169.254/latest/meta-data/iam/')).rejects.toThrow(/blocked/i) + await expect(navigate('http://metadata.google.internal/computeMetadata/v1/')).rejects.toThrow(/blocked/i) + await expect(navigate('http://168.63.129.16/metadata/instance')).rejects.toThrow(/blocked/i) + }) + it('rejects internal domains and credentialed urls', async () => { await expect(navigate('http://db.internal')).rejects.toThrow(/blocked/i) await expect(navigate('https://user:pass@example.com')).rejects.toThrow(/blocked/i)