Skip to content

Commit b66e6c6

Browse files
committed
fix(core): add boundary assertion to hasLiteralQuery regex and address edge cases
backporting from 7e25ee3
1 parent 749fc4b commit b66e6c6

File tree

2 files changed

+34
-4
lines changed

2 files changed

+34
-4
lines changed

src/shared/uriTemplate.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,14 +287,23 @@ export class UriTemplate {
287287
let queryPart: string;
288288

289289
if (hasLiteralQuery) {
290-
// Match the path regex against the full URI without a trailing
291-
// anchor, then treat everything after the match as the remaining
292-
// query string to parse for {?...}/{&...} expressions.
290+
// Match the path regex against the full URI. The lookahead
291+
// assertion ensures the literal portion ends exactly at a
292+
// query-param separator, fragment, or end-of-string, so a
293+
// template like `?id=1` does not spuriously prefix-match
294+
// `?id=100`.
295+
pattern += '(?=[&#]|$)';
293296
UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, 'Generated regex pattern');
294297
const regex = new RegExp(pattern);
295298
match = uri.match(regex);
296299
if (!match) return null;
297-
queryPart = uri.slice(match[0].length).replace(/^&/, '');
300+
// Everything after the match is the remaining query string to
301+
// parse for {?...}/{&...} expressions. Strip any fragment and
302+
// the leading `&` separator first.
303+
let rest = uri.slice(match[0].length);
304+
const hashIndex = rest.indexOf('#');
305+
if (hashIndex !== -1) rest = rest.slice(0, hashIndex);
306+
queryPart = rest.replace(/^&/, '');
298307
} else {
299308
// Split URI into path and query parts at the first '?'
300309
const queryIndex = uri.indexOf('?');

test/shared/uriTemplate.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,27 @@ describe('UriTemplate', () => {
280280
const match = template.match('/api/v1?format=json&key=secret');
281281
expect(match).toEqual({ version: 'v1', key: 'secret' });
282282
});
283+
284+
285+
it('should not prefix-match literal query values with literal ?', () => {
286+
// `?id=1` must not match `?id=100`
287+
const template = new UriTemplate('/path?id=1{&extra}');
288+
expect(template.match('/path?id=100')).toBeNull();
289+
expect(template.match('/path?id=1')).toEqual({});
290+
expect(template.match('/path?id=1&extra=x')).toEqual({ extra: 'x' });
291+
});
292+
293+
it('should require a proper separator after the literal ? portion', () => {
294+
// malformed URI missing `&` between params must not match
295+
const template = new UriTemplate('/path?a=1{&b}');
296+
expect(template.match('/path?a=1b=2')).toBeNull();
297+
});
298+
299+
it('should ignore fragments after the literal ? portion', () => {
300+
const template = new UriTemplate('/path?a=1{&b}');
301+
expect(template.match('/path?a=1#section')).toEqual({});
302+
expect(template.match('/path?a=1&b=foo#section')).toEqual({ b: 'foo' });
303+
});
283304
});
284305

285306
describe('security and edge cases', () => {

0 commit comments

Comments
 (0)