22
33type Variables = Record < string , string | string [ ] > ;
44
5+ const MAX_TEMPLATE_LENGTH = 1000000 ; // 1MB
6+ const MAX_VARIABLE_LENGTH = 1000000 ; // 1MB
7+ const MAX_TEMPLATE_EXPRESSIONS = 10000 ;
8+ const MAX_REGEX_LENGTH = 1000000 ; // 1MB
9+
510export class UriTemplate {
11+ private static validateLength ( str : string , max : number , context : string ) : void {
12+ if ( str . length > max ) {
13+ throw new Error (
14+ `${ context } exceeds maximum length of ${ max } characters (got ${ str . length } )` ,
15+ ) ;
16+ }
17+ }
618 private readonly parts : Array <
719 | string
820 | { name : string ; operator : string ; names : string [ ] ; exploded : boolean }
921 > ;
1022
1123 constructor ( template : string ) {
24+ UriTemplate . validateLength ( template , MAX_TEMPLATE_LENGTH , "Template" ) ;
1225 this . parts = this . parse ( template ) ;
1326 }
1427
@@ -24,6 +37,7 @@ export class UriTemplate {
2437 > = [ ] ;
2538 let currentText = "" ;
2639 let i = 0 ;
40+ let expressionCount = 0 ;
2741
2842 while ( i < template . length ) {
2943 if ( template [ i ] === "{" ) {
@@ -34,11 +48,28 @@ export class UriTemplate {
3448 const end = template . indexOf ( "}" , i ) ;
3549 if ( end === - 1 ) throw new Error ( "Unclosed template expression" ) ;
3650
51+ expressionCount ++ ;
52+ if ( expressionCount > MAX_TEMPLATE_EXPRESSIONS ) {
53+ throw new Error (
54+ `Template contains too many expressions (max ${ MAX_TEMPLATE_EXPRESSIONS } )` ,
55+ ) ;
56+ }
57+
3758 const expr = template . slice ( i + 1 , end ) ;
3859 const operator = this . getOperator ( expr ) ;
3960 const exploded = expr . includes ( "*" ) ;
4061 const names = this . getNames ( expr ) ;
4162 const name = names [ 0 ] ;
63+
64+ // Validate variable name length
65+ for ( const name of names ) {
66+ UriTemplate . validateLength (
67+ name ,
68+ MAX_VARIABLE_LENGTH ,
69+ "Variable name" ,
70+ ) ;
71+ }
72+
4273 parts . push ( { name, operator, names, exploded } ) ;
4374 i = end + 1 ;
4475 } else {
@@ -69,6 +100,7 @@ export class UriTemplate {
69100 }
70101
71102 private encodeValue ( value : string , operator : string ) : string {
103+ UriTemplate . validateLength ( value , MAX_VARIABLE_LENGTH , "Variable value" ) ;
72104 if ( operator === "+" || operator === "#" ) {
73105 return encodeURI ( value ) ;
74106 }
@@ -132,12 +164,31 @@ export class UriTemplate {
132164 }
133165
134166 expand ( variables : Variables ) : string {
135- return this . parts
136- . map ( ( part ) => {
137- if ( typeof part === "string" ) return part ;
138- return this . expandPart ( part , variables ) ;
139- } )
140- . join ( "" ) ;
167+ let result = "" ;
168+ let hasQueryParam = false ;
169+
170+ for ( const part of this . parts ) {
171+ if ( typeof part === "string" ) {
172+ result += part ;
173+ continue ;
174+ }
175+
176+ const expanded = this . expandPart ( part , variables ) ;
177+ if ( ! expanded ) continue ;
178+
179+ // Convert ? to & if we already have a query parameter
180+ if ( ( part . operator === "?" || part . operator === "&" ) && hasQueryParam ) {
181+ result += expanded . replace ( "?" , "&" ) ;
182+ } else {
183+ result += expanded ;
184+ }
185+
186+ if ( part . operator === "?" || part . operator === "&" ) {
187+ hasQueryParam = true ;
188+ }
189+ }
190+
191+ return result ;
141192 }
142193
143194 private escapeRegExp ( str : string ) : string {
@@ -152,6 +203,11 @@ export class UriTemplate {
152203 } ) : Array < { pattern : string ; name : string } > {
153204 const patterns : Array < { pattern : string ; name : string } > = [ ] ;
154205
206+ // Validate variable name length for matching
207+ for ( const name of part . names ) {
208+ UriTemplate . validateLength ( name , MAX_VARIABLE_LENGTH , "Variable name" ) ;
209+ }
210+
155211 if ( part . operator === "?" || part . operator === "&" ) {
156212 for ( let i = 0 ; i < part . names . length ; i ++ ) {
157213 const name = part . names [ i ] ;
@@ -190,6 +246,7 @@ export class UriTemplate {
190246 }
191247
192248 match ( uri : string ) : Variables | null {
249+ UriTemplate . validateLength ( uri , MAX_TEMPLATE_LENGTH , "URI" ) ;
193250 let pattern = "^" ;
194251 const names : Array < { name : string ; exploded : boolean } > = [ ] ;
195252
@@ -206,6 +263,7 @@ export class UriTemplate {
206263 }
207264
208265 pattern += "$" ;
266+ UriTemplate . validateLength ( pattern , MAX_REGEX_LENGTH , "Generated regex pattern" ) ;
209267 const regex = new RegExp ( pattern ) ;
210268 const match = uri . match ( regex ) ;
211269
0 commit comments