diff --git a/packages/platform-server/src/location.ts b/packages/platform-server/src/location.ts index 3edcc6d3954..936c4f6a81a 100644 --- a/packages/platform-server/src/location.ts +++ b/packages/platform-server/src/location.ts @@ -18,29 +18,18 @@ import {Subject} from 'rxjs'; import {INITIAL_CONFIG} from './tokens'; -function parseUrl( - urlStr: string, - origin: string, -): { - hostname: string; - protocol: string; - port: string; - pathname: string; - search: string; - hash: string; - href: string; -} { - const {hostname, protocol, port, pathname, search, hash, href} = new URL(urlStr, origin); +/** + * Parses a URL string and returns a URL object. + * @param urlStr The string to parse. + * @param origin The origin to use for resolving the URL. + * @returns The parsed URL. + */ +function parseUrl(urlStr: string, origin: string): URL { + // If the URL is empty or start with a `/` it is a pathname relative to the origin + // otherwise it's an absolute URL. + const urlToParse = urlStr.length === 0 || urlStr[0] === '/' ? origin + urlStr : urlStr; - return { - hostname, - href, - protocol, - port, - pathname, - search, - hash, - }; + return new URL(urlToParse); } /** @@ -65,14 +54,17 @@ export class ServerPlatformLocation implements PlatformLocation { return; } if (config.url) { - const url = parseUrl(config.url, this._doc.location.origin); - this.protocol = url.protocol; - this.hostname = url.hostname; - this.port = url.port; - this.pathname = url.pathname; - this.search = url.search; - this.hash = url.hash; - this.href = url.href; + const {protocol, hostname, port, pathname, search, hash, href} = parseUrl( + config.url, + this._doc.location.origin, + ); + this.protocol = protocol; + this.hostname = hostname; + this.port = port; + this.pathname = pathname; + this.search = search; + this.hash = hash; + this.href = href; } } @@ -114,12 +106,13 @@ export class ServerPlatformLocation implements PlatformLocation { replaceState(state: any, title: string, newUrl: string): void { const oldUrl = this.url; - const parsedUrl = parseUrl(newUrl, this._doc.location.origin); - (this as Writable).pathname = parsedUrl.pathname; - (this as Writable).search = parsedUrl.search; - (this as Writable).href = parsedUrl.href; - (this as Writable).protocol = parsedUrl.protocol; - this.setHash(parsedUrl.hash, oldUrl); + const {pathname, search, hash, href, protocol} = parseUrl(newUrl, this._doc.location.origin); + const writableThis = this as Writable; + writableThis.pathname = pathname; + writableThis.search = search; + writableThis.href = href; + writableThis.protocol = protocol; + this.setHash(hash, oldUrl); } pushState(state: any, title: string, newUrl: string): void { diff --git a/packages/platform-server/test/platform_location_spec.ts b/packages/platform-server/test/platform_location_spec.ts index 74d48dd727a..2dae5e9fb06 100644 --- a/packages/platform-server/test/platform_location_spec.ts +++ b/packages/platform-server/test/platform_location_spec.ts @@ -8,20 +8,13 @@ import '@angular/compiler'; import {PlatformLocation, ɵgetDOM as getDOM} from '@angular/common'; -import {Component, destroyPlatform} from '@angular/core'; +import {destroyPlatform} from '@angular/core'; import {INITIAL_CONFIG, platformServer} from '@angular/platform-server'; -import {bootstrapApplication} from '@angular/platform-browser'; (function () { if (getDOM().supportsDOMEvents) return; // NODE only describe('PlatformLocation', () => { - @Component({ - selector: 'app', - template: `Works!`, - }) - class LocationApp {} - beforeEach(() => { destroyPlatform(); }); @@ -34,15 +27,8 @@ import {bootstrapApplication} from '@angular/platform-browser'; const platform = platformServer([ {provide: INITIAL_CONFIG, useValue: {document: ''}}, ]); - const appRef = await bootstrapApplication( - LocationApp, - { - providers: [{provide: INITIAL_CONFIG, useValue: {document: ''}}], - }, - {platformRef: platform}, - ); - const location = appRef.injector.get(PlatformLocation); + const location = platform.injector.get(PlatformLocation); expect(location.pathname).toBe('/'); platform.destroy(); }); @@ -57,23 +43,7 @@ import {bootstrapApplication} from '@angular/platform-browser'; }, ]); - const appRef = await bootstrapApplication( - LocationApp, - { - providers: [ - { - provide: INITIAL_CONFIG, - useValue: { - document: '', - url: 'http://test.com/deep/path?query#hash', - }, - }, - ], - }, - {platformRef: platform}, - ); - - const location = appRef.injector.get(PlatformLocation); + const location = platform.injector.get(PlatformLocation); expect(location.pathname).toBe('/deep/path'); expect(location.search).toBe('?query'); expect(location.hash).toBe('#hash'); @@ -90,23 +60,7 @@ import {bootstrapApplication} from '@angular/platform-browser'; }, ]); - const appRef = await bootstrapApplication( - LocationApp, - { - providers: [ - { - provide: INITIAL_CONFIG, - useValue: { - document: '', - url: 'http://test.com:80/deep/path?query#hash', - }, - }, - ], - }, - {platformRef: platform}, - ); - - const location = appRef.injector.get(PlatformLocation); + const location = platform.injector.get(PlatformLocation); expect(location.hostname).toBe('test.com'); expect(location.protocol).toBe('http:'); expect(location.port).toBe(''); @@ -126,23 +80,7 @@ import {bootstrapApplication} from '@angular/platform-browser'; }, ]); - const appRef = await bootstrapApplication( - LocationApp, - { - providers: [ - { - provide: INITIAL_CONFIG, - useValue: { - document: '', - url: 'http://test.com/deep/path', - }, - }, - ], - }, - {platformRef: platform}, - ); - - const location = appRef.injector.get(PlatformLocation); + const location = platform.injector.get(PlatformLocation); expect(location.pathname).toBe('/deep/path'); expect(location.search).toBe(''); expect(location.hash).toBe(''); @@ -153,14 +91,7 @@ import {bootstrapApplication} from '@angular/platform-browser'; {provide: INITIAL_CONFIG, useValue: {document: ''}}, ]); - const appRef = await bootstrapApplication( - LocationApp, - { - providers: [{provide: INITIAL_CONFIG, useValue: {document: ''}}], - }, - {platformRef: platform}, - ); - const location = appRef.injector.get(PlatformLocation); + const location = platform.injector.get(PlatformLocation); location.pushState(null, 'Test', '/foo#bar'); expect(location.pathname).toBe('/foo'); expect(location.hash).toBe('#bar'); @@ -178,22 +109,7 @@ import {bootstrapApplication} from '@angular/platform-browser'; }, ]); - const appRef = await bootstrapApplication( - LocationApp, - { - providers: [ - { - provide: INITIAL_CONFIG, - useValue: { - document: '', - url: 'http://test.com/deep/path?query#hash', - }, - }, - ], - }, - {platformRef: platform}, - ); - const location = appRef.injector.get(PlatformLocation); + const location = platform.injector.get(PlatformLocation); location.replaceState(null, 'Test', '/foo#bar'); expect(location.pathname).toBe('/foo'); expect(location.hash).toBe('#bar'); @@ -206,24 +122,42 @@ import {bootstrapApplication} from '@angular/platform-browser'; const platform = platformServer([ {provide: INITIAL_CONFIG, useValue: {document: ''}}, ]); - bootstrapApplication( - LocationApp, - { - providers: [{provide: INITIAL_CONFIG, useValue: {document: ''}}], - }, - {platformRef: platform}, - ).then((appRef) => { - const location: PlatformLocation = appRef.injector.get(PlatformLocation); - expect(location.pathname).toBe('/'); - location.onHashChange((e: any) => { - expect(e.type).toBe('hashchange'); - expect(e.oldUrl).toBe('/'); - expect(e.newUrl).toBe('/foo#bar'); - platform.destroy(); - done(); - }); - location.pushState(null, 'Test', '/foo#bar'); + const location = platform.injector.get(PlatformLocation); + + expect(location.pathname).toBe('/'); + location.onHashChange((e: any) => { + expect(e.type).toBe('hashchange'); + expect(e.oldUrl).toBe('/'); + expect(e.newUrl).toBe('/foo#bar'); + platform.destroy(); + done(); }); + location.pushState(null, 'Test', '/foo#bar'); + }); + + it('neutralizes hostname hijack attempts', async () => { + const urls = ['/\\attacker.com/deep/path', '//attacker.com/deep/path']; + + for (const url of urls) { + const platform = platformServer([ + { + provide: INITIAL_CONFIG, + useValue: { + document: '', + // This should be treated as relative URL. + // Example: `req.url: '//attacker.com/deep/path'` where request + // to express server is 'http://localhost:4200//attacker.com/deep/path'. + url, + }, + }, + ]); + + const location = platform.injector.get(PlatformLocation); + platform.destroy(); + + expect(location.hostname).withContext(`hostname for URL: "${url}"`).toBe(''); + expect(location.pathname).withContext(`pathname for URL: "${url}"`).toBe(url); + } }); }); })();