|
7 | 7 | */ |
8 | 8 | import '@angular/compiler'; |
9 | 9 |
|
10 | | -import {PlatformLocation, ɵgetDOM as getDOM} from '@angular/common'; |
| 10 | +import {DOCUMENT, PlatformLocation, ɵgetDOM as getDOM} from '@angular/common'; |
11 | 11 | import {destroyPlatform} from '@angular/core'; |
12 | 12 | import {INITIAL_CONFIG, platformServer} from '@angular/platform-server'; |
13 | 13 |
|
14 | | -import {parseUrl} from '../src/location'; |
15 | | - |
16 | 14 | (function () { |
17 | 15 | if (getDOM().supportsDOMEvents) return; // NODE only |
18 | 16 |
|
19 | | - describe('parseUrl', () => { |
20 | | - it('should resolve relative paths against origin', () => { |
21 | | - const url = parseUrl('/deep/path?query#hash', 'http://test.com'); |
22 | | - expect(url.href).toBe('http://test.com/deep/path?query#hash'); |
23 | | - expect(url.search).toBe('?query'); |
24 | | - expect(url.hash).toBe('#hash'); |
25 | | - }); |
26 | | - |
27 | | - it('should resolve absolute URLs ignoring origin', () => { |
28 | | - const url = parseUrl('http://other.com/deep/path', 'http://test.com'); |
29 | | - expect(url.href).toBe('http://other.com/deep/path'); |
30 | | - expect(url.origin).toBe('http://other.com'); |
31 | | - }); |
32 | | - }); |
33 | | - |
34 | 17 | describe('PlatformLocation', () => { |
35 | 18 | beforeEach(() => { |
36 | 19 | destroyPlatform(); |
@@ -173,7 +156,72 @@ import {parseUrl} from '../src/location'; |
173 | 156 | platform.destroy(); |
174 | 157 |
|
175 | 158 | expect(location.hostname).withContext(`hostname for URL: "${url}"`).toBe(''); |
176 | | - expect(location.pathname).withContext(`pathname for URL: "${url}"`).toBe(url); |
| 159 | + expect(location.pathname) |
| 160 | + .withContext(`pathname for URL: "${url}"`) |
| 161 | + .toBe('/attacker.com/deep/path'); |
| 162 | + } |
| 163 | + }); |
| 164 | + |
| 165 | + it('should set the proper document location when the URL has leading slashes to prevent origin hijack', async () => { |
| 166 | + const urls = ['/\\attacker.com/deep/path', '//attacker.com/deep/path']; |
| 167 | + |
| 168 | + for (const url of urls) { |
| 169 | + const platform = platformServer([ |
| 170 | + { |
| 171 | + provide: INITIAL_CONFIG, |
| 172 | + useValue: { |
| 173 | + document: '<html><head></head><body></body></html>', |
| 174 | + url, |
| 175 | + }, |
| 176 | + }, |
| 177 | + ]); |
| 178 | + |
| 179 | + const doc = platform.injector.get(DOCUMENT); |
| 180 | + platform.destroy(); |
| 181 | + |
| 182 | + expect(doc.location.origin).not.toBe('http://attacker.com'); |
| 183 | + expect(doc.location.pathname).toBe('/attacker.com/deep/path'); |
| 184 | + } |
| 185 | + }); |
| 186 | + |
| 187 | + it('should not expose protocol-relative URLs on the location to prevent open redirect and SSRF bypasses', async () => { |
| 188 | + const urls = ['/\\attacker.com/deep/path', '//attacker.com/deep/path']; |
| 189 | + const origins = [undefined, 'http://localhost:4200']; |
| 190 | + |
| 191 | + for (const url of urls) { |
| 192 | + for (const origin of origins) { |
| 193 | + const providers: any[] = [ |
| 194 | + { |
| 195 | + provide: INITIAL_CONFIG, |
| 196 | + useValue: { |
| 197 | + document: '', |
| 198 | + url, |
| 199 | + }, |
| 200 | + }, |
| 201 | + ]; |
| 202 | + |
| 203 | + if (origin) { |
| 204 | + providers.push({ |
| 205 | + provide: DOCUMENT, |
| 206 | + useValue: { |
| 207 | + location: { |
| 208 | + origin, |
| 209 | + }, |
| 210 | + }, |
| 211 | + }); |
| 212 | + } |
| 213 | + |
| 214 | + const platform = platformServer(providers); |
| 215 | + const location = platform.injector.get(PlatformLocation) as any; |
| 216 | + platform.destroy(); |
| 217 | + |
| 218 | + // A relative redirect URL starting with // or /\ is normalized by browsers to a protocol-relative URL. |
| 219 | + // The PlatformLocation.url property MUST NOT expose these unsafe patterns. |
| 220 | + const isVulnerable = location.url.startsWith('//') || location.url.startsWith('/\\'); |
| 221 | + expect(isVulnerable) |
| 222 | + .withContext(`URL: "${url}", origin: "${origin}", location.url: "${location.url}"`) |
| 223 | + .toBeFalse(); |
| 224 | + } |
177 | 225 | } |
178 | 226 | }); |
179 | 227 | }); |
|
0 commit comments