Skip to content

Commit 7671c73

Browse files
committedMar 29, 2019
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
1 parent 6d7a4ff commit 7671c73

File tree

2 files changed

+67
-12
lines changed

2 files changed

+67
-12
lines changed
 

‎packages/common/src/viewport_scroller.ts

+27-12
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {defineInjectable, inject} from '@angular/core';
9+
import {ErrorHandler, defineInjectable, inject} from '@angular/core';
1010

1111
import {DOCUMENT} from './dom_tokens';
1212

13+
14+
1315
/**
1416
* Defines a scroll position manager. Implemented by `BrowserViewportScroller`.
1517
*
@@ -19,8 +21,10 @@ export abstract class ViewportScroller {
1921
// De-sugared tree-shakable injection
2022
// See #23917
2123
/** @nocollapse */
22-
static ngInjectableDef = defineInjectable(
23-
{providedIn: 'root', factory: () => new BrowserViewportScroller(inject(DOCUMENT), window)});
24+
static ngInjectableDef = defineInjectable({
25+
providedIn: 'root',
26+
factory: () => new BrowserViewportScroller(inject(DOCUMENT), window, inject(ErrorHandler))
27+
});
2428

2529
/**
2630
* Configures the top offset used when scrolling to an anchor.
@@ -62,7 +66,7 @@ export abstract class ViewportScroller {
6266
export class BrowserViewportScroller implements ViewportScroller {
6367
private offset: () => [number, number] = () => [0, 0];
6468

65-
constructor(private document: any, private window: any) {}
69+
constructor(private document: any, private window: any, private errorHandler: ErrorHandler) {}
6670

6771
/**
6872
* Configures the top offset used when scrolling to an anchor.
@@ -106,15 +110,26 @@ export class BrowserViewportScroller implements ViewportScroller {
106110
*/
107111
scrollToAnchor(anchor: string): void {
108112
if (this.supportScrollRestoration()) {
109-
const elSelectedById = this.document.querySelector(`#${anchor}`);
110-
if (elSelectedById) {
111-
this.scrollToElement(elSelectedById);
112-
return;
113+
// Escape anything passed to `querySelector` as it can throw errors and stop the application
114+
// from working if invalid values are passed.
115+
if (this.window.CSS && this.window.CSS.escape) {
116+
anchor = this.window.CSS.escape(anchor);
117+
} else {
118+
anchor = anchor.replace(/(\"|\'\ |:|\.|\[|\]|,|=)/g, '\\$1');
113119
}
114-
const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
115-
if (elSelectedByName) {
116-
this.scrollToElement(elSelectedByName);
117-
return;
120+
try {
121+
const elSelectedById = this.document.querySelector(`#${anchor}`);
122+
if (elSelectedById) {
123+
this.scrollToElement(elSelectedById);
124+
return;
125+
}
126+
const elSelectedByName = this.document.querySelector(`[name='${anchor}']`);
127+
if (elSelectedByName) {
128+
this.scrollToElement(elSelectedByName);
129+
return;
130+
}
131+
} catch (e) {
132+
this.errorHandler.handleError(e);
118133
}
119134
}
120135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
10+
11+
/**
12+
* @license
13+
* Copyright Google Inc. All Rights Reserved.
14+
*
15+
* Use of this source code is governed by an MIT-style license that can be
16+
* found in the LICENSE file at https://angular.io/license
17+
*/
18+
19+
import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
20+
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
21+
22+
{
23+
describe('BrowserViewportScroller', () => {
24+
let scroller: ViewportScroller;
25+
let documentSpy: any;
26+
beforeEach(() => {
27+
documentSpy = jasmine.createSpyObj('document', ['querySelector']);
28+
scroller = new BrowserViewportScroller(documentSpy, {scrollTo: 1}, null !);
29+
});
30+
it('escapes invalid characters selectors', () => {
31+
const invalidSelectorChars = `"' :.[],=`;
32+
// Double escaped to make sure we match the actual value passed to `querySelector`
33+
const escapedInvalids = `\\"\\' \\:\\.\\[\\]\\,\\=`;
34+
scroller.scrollToAnchor(`specials=${invalidSelectorChars}`);
35+
expect(documentSpy.querySelector).toHaveBeenCalledWith(`#specials\\=${escapedInvalids}`);
36+
expect(documentSpy.querySelector)
37+
.toHaveBeenCalledWith(`[name='specials\\=${escapedInvalids}']`);
38+
});
39+
});
40+
}

0 commit comments

Comments
 (0)
Please sign in to comment.