From 523c032c9d040c6b86932062f85fd9d9ec5e97b4 Mon Sep 17 00:00:00 2001 From: ubeddulla khan Date: Wed, 8 Jul 2026 11:58:44 +0530 Subject: [PATCH 1/2] escape rootURL and baseURL before regexp in getURL --- packages/@ember/routing/history-location.ts | 6 +++--- packages/@ember/routing/lib/location-utils.ts | 10 ++++++++++ packages/@ember/routing/none-location.ts | 3 ++- .../tests/location/history_location_test.js | 17 +++++++++++++++++ .../tests/location/none_location_test.js | 14 ++++++++++++++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/@ember/routing/history-location.ts b/packages/@ember/routing/history-location.ts index 42019c63b54..786a8fab83d 100644 --- a/packages/@ember/routing/history-location.ts +++ b/packages/@ember/routing/history-location.ts @@ -1,7 +1,7 @@ import EmberObject from '@ember/object'; import { assert } from '@ember/debug'; import type { default as EmberLocation, UpdateCallback } from '@ember/routing/location'; -import { getHash } from './lib/location-utils'; +import { escapeRegExp, getHash } from './lib/location-utils'; /** @module @ember/routing/history-location @@ -134,8 +134,8 @@ export default class HistoryLocation extends EmberObject implements EmberLocatio // remove baseURL and rootURL from start of path let url = path - .replace(new RegExp(`^${baseURL}(?=/|$)`), '') - .replace(new RegExp(`^${rootURL}(?=/|$)`), '') + .replace(new RegExp(`^${escapeRegExp(baseURL)}(?=/|$)`), '') + .replace(new RegExp(`^${escapeRegExp(rootURL)}(?=/|$)`), '') .replace(/\/\//g, '/'); // remove extra slashes let search = location.search || ''; diff --git a/packages/@ember/routing/lib/location-utils.ts b/packages/@ember/routing/lib/location-utils.ts index 0fe26430886..b03ded32b51 100644 --- a/packages/@ember/routing/lib/location-utils.ts +++ b/packages/@ember/routing/lib/location-utils.ts @@ -1,3 +1,13 @@ +/** + @private + + Escapes any regular-expression metacharacters in `str` so it can be safely + interpolated into a `RegExp` and matched as a literal. +*/ +export function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + /** @private diff --git a/packages/@ember/routing/none-location.ts b/packages/@ember/routing/none-location.ts index 1e43888207f..e396d8ef335 100644 --- a/packages/@ember/routing/none-location.ts +++ b/packages/@ember/routing/none-location.ts @@ -1,6 +1,7 @@ import EmberObject from '@ember/object'; import { assert } from '@ember/debug'; import type { default as EmberLocation, UpdateCallback } from '@ember/routing/location'; +import { escapeRegExp } from './lib/location-utils'; /** @module @ember/routing/none-location @@ -63,7 +64,7 @@ export default class NoneLocation extends EmberObject implements EmberLocation { rootURL = rootURL.replace(/\/$/, ''); // remove rootURL from url - return path.replace(new RegExp(`^${rootURL}(?=/|$)`), ''); + return path.replace(new RegExp(`^${escapeRegExp(rootURL)}(?=/|$)`), ''); } /** diff --git a/packages/@ember/routing/tests/location/history_location_test.js b/packages/@ember/routing/tests/location/history_location_test.js index 984dad9c778..68505a5a3ec 100644 --- a/packages/@ember/routing/tests/location/history_location_test.js +++ b/packages/@ember/routing/tests/location/history_location_test.js @@ -319,6 +319,23 @@ moduleFor( assert.equal(location.getURL(), '/admin/profile/'); } + ['@test HistoryLocation.getURL() treats regex metacharacters in rootURL and baseURL literally']( + assert + ) { + HistoryTestLocation.reopen({ + init() { + this._super(...arguments); + set(this, 'location', mockBrowserLocation('/axc/secret')); + set(this, 'rootURL', '/a.c/'); + set(this, 'baseURL', '/a.c/'); + }, + }); + + createLocation(); + + assert.equal(location.getURL(), '/axc/secret'); + } + ['@test Existing state is preserved on init'](assert) { let existingState = { path: '/route/path', diff --git a/packages/@ember/routing/tests/location/none_location_test.js b/packages/@ember/routing/tests/location/none_location_test.js index 3257c885eb4..78be7ecdd8c 100644 --- a/packages/@ember/routing/tests/location/none_location_test.js +++ b/packages/@ember/routing/tests/location/none_location_test.js @@ -84,5 +84,19 @@ moduleFor( assert.equal(location.getURL(), '/bars/baz'); } + + ['@test NoneLocation.getURL() treats regex metacharacters in rootURL literally'](assert) { + NoneTestLocation.reopen({ + init() { + this._super(...arguments); + set(this, 'rootURL', '/a.c/'); + set(this, 'path', '/axc/secret'); + }, + }); + + createLocation(); + + assert.equal(location.getURL(), '/axc/secret'); + } } ); From b9994d376da3024ffcc08f64874e5890a34aff37 Mon Sep 17 00:00:00 2001 From: ubeddulla khan Date: Thu, 9 Jul 2026 10:42:42 +0530 Subject: [PATCH 2/2] note RegExp.escape follow-up and cover extra-slash rootURL --- packages/@ember/routing/lib/location-utils.ts | 3 +++ .../routing/tests/location/none_location_test.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/@ember/routing/lib/location-utils.ts b/packages/@ember/routing/lib/location-utils.ts index b03ded32b51..e7e16102ebe 100644 --- a/packages/@ember/routing/lib/location-utils.ts +++ b/packages/@ember/routing/lib/location-utils.ts @@ -3,6 +3,9 @@ Escapes any regular-expression metacharacters in `str` so it can be safely interpolated into a `RegExp` and matched as a literal. + + TODO: delete this in favor of `RegExp.escape` once our minimum supported + browsers all include it (https://caniuse.com/mdn-javascript_builtins_regexp_escape). */ export function escapeRegExp(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); diff --git a/packages/@ember/routing/tests/location/none_location_test.js b/packages/@ember/routing/tests/location/none_location_test.js index 78be7ecdd8c..04f77db057b 100644 --- a/packages/@ember/routing/tests/location/none_location_test.js +++ b/packages/@ember/routing/tests/location/none_location_test.js @@ -98,5 +98,19 @@ moduleFor( assert.equal(location.getURL(), '/axc/secret'); } + + ['@test NoneLocation.getURL() strips the rootURL when it has an extra trailing slash'](assert) { + NoneTestLocation.reopen({ + init() { + this._super(...arguments); + set(this, 'rootURL', '/foo//'); + set(this, 'path', '/foo//bar'); + }, + }); + + createLocation(); + + assert.equal(location.getURL(), '/bar'); + } } );