fix(common): escape query selector used when anchor scrolling (#29577)

When an anchor scroll happens, we run document.querySelector. This value can be taken directly from the user. Therefore it's possible to throw an error on scrolling, which can cause the application to fail.

This PR escapes the selector before using it.

Related to #28193
[Internal discussion](https://groups.google.com/a/google.com/forum/#!topic/angular-users/d82GHfmRKLc)

PR Close #29577
This commit is contained in:
Jason Aden 2019-03-28 15:00:51 -07:00
parent 303eae918d
commit e8768acacc
2 changed files with 67 additions and 12 deletions

View File

@ -6,10 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {defineInjectable, inject} from '@angular/core';
import {ErrorHandler, defineInjectable, inject} from '@angular/core';
import {DOCUMENT} from './dom_tokens';
/**
* Defines a scroll position manager. Implemented by `BrowserViewportScroller`.
*
@ -19,8 +21,10 @@ export abstract class ViewportScroller {
// De-sugared tree-shakable injection
// See #23917
/** @nocollapse */
static ngInjectableDef = defineInjectable(
{providedIn: 'root', factory: () => new BrowserViewportScroller(inject(DOCUMENT), window)});
static ngInjectableDef = defineInjectable({
providedIn: 'root',
factory: () => new BrowserViewportScroller(inject(DOCUMENT), window, inject(ErrorHandler))
});
/**
* Configures the top offset used when scrolling to an anchor.
@ -62,7 +66,7 @@ export abstract class ViewportScroller {
export class BrowserViewportScroller implements ViewportScroller {
private offset: () => [number, number] = () => [0, 0];
constructor(private document: any, private window: any) {}
constructor(private document: any, private window: any, private errorHandler: ErrorHandler) {}
/**
* Configures the top offset used when scrolling to an anchor.
@ -106,15 +110,26 @@ export class BrowserViewportScroller implements ViewportScroller {
*/
scrollToAnchor(anchor: string): void {
if (this.supportScrollRestoration()) {
const elSelectedById = this.document.querySelector(`#${anchor}`);
if (elSelectedById) {
this.scrollToElement(elSelectedById);
return;
// Escape anything passed to `querySelector` as it can throw errors and stop the application
// from working if invalid values are passed.
if (this.window.CSS && this.window.CSS.escape) {
anchor = this.window.CSS.escape(anchor);
} else {
anchor = anchor.replace(/(\"|\'\ |:|\.|\[|\]|,|=)/g, '\\$1');
}
const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
if (elSelectedByName) {
this.scrollToElement(elSelectedByName);
return;
try {
const elSelectedById = this.document.querySelector(`#${anchor}`);
if (elSelectedById) {
this.scrollToElement(elSelectedById);
return;
}
const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
if (elSelectedByName) {
this.scrollToElement(elSelectedByName);
return;
}
} catch (e) {
this.errorHandler.handleError(e);
}
}
}

View File

@ -0,0 +1,40 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
{
describe('BrowserViewportScroller', () => {
let scroller: ViewportScroller;
let documentSpy: any;
beforeEach(() => {
documentSpy = jasmine.createSpyObj('document', ['querySelector']);
scroller = new BrowserViewportScroller(documentSpy, {scrollTo: 1}, null !);
});
it('escapes invalid characters selectors', () => {
const invalidSelectorChars = `"' :.[],=`;
// Double escaped to make sure we match the actual value passed to `querySelector`
const escapedInvalids = `\\"\\' \\:\\.\\[\\]\\,\\=`;
scroller.scrollToAnchor(`specials=${invalidSelectorChars}`);
expect(documentSpy.querySelector).toHaveBeenCalledWith(`#specials\\=${escapedInvalids}`);
expect(documentSpy.querySelector)
.toHaveBeenCalledWith(`[name='specials\\=${escapedInvalids}']`);
});
});
}