2
2
3
3
type Variables = Record < string , string | string [ ] > ;
4
4
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
+
5
10
export 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
+ }
6
18
private readonly parts : Array <
7
19
| string
8
20
| { name : string ; operator : string ; names : string [ ] ; exploded : boolean }
9
21
> ;
10
22
11
23
constructor ( template : string ) {
24
+ UriTemplate . validateLength ( template , MAX_TEMPLATE_LENGTH , "Template" ) ;
12
25
this . parts = this . parse ( template ) ;
13
26
}
14
27
@@ -24,6 +37,7 @@ export class UriTemplate {
24
37
> = [ ] ;
25
38
let currentText = "" ;
26
39
let i = 0 ;
40
+ let expressionCount = 0 ;
27
41
28
42
while ( i < template . length ) {
29
43
if ( template [ i ] === "{" ) {
@@ -34,11 +48,28 @@ export class UriTemplate {
34
48
const end = template . indexOf ( "}" , i ) ;
35
49
if ( end === - 1 ) throw new Error ( "Unclosed template expression" ) ;
36
50
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
+
37
58
const expr = template . slice ( i + 1 , end ) ;
38
59
const operator = this . getOperator ( expr ) ;
39
60
const exploded = expr . includes ( "*" ) ;
40
61
const names = this . getNames ( expr ) ;
41
62
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
+
42
73
parts . push ( { name, operator, names, exploded } ) ;
43
74
i = end + 1 ;
44
75
} else {
@@ -69,6 +100,7 @@ export class UriTemplate {
69
100
}
70
101
71
102
private encodeValue ( value : string , operator : string ) : string {
103
+ UriTemplate . validateLength ( value , MAX_VARIABLE_LENGTH , "Variable value" ) ;
72
104
if ( operator === "+" || operator === "#" ) {
73
105
return encodeURI ( value ) ;
74
106
}
@@ -132,12 +164,31 @@ export class UriTemplate {
132
164
}
133
165
134
166
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 ;
141
192
}
142
193
143
194
private escapeRegExp ( str : string ) : string {
@@ -152,6 +203,11 @@ export class UriTemplate {
152
203
} ) : Array < { pattern : string ; name : string } > {
153
204
const patterns : Array < { pattern : string ; name : string } > = [ ] ;
154
205
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
+
155
211
if ( part . operator === "?" || part . operator === "&" ) {
156
212
for ( let i = 0 ; i < part . names . length ; i ++ ) {
157
213
const name = part . names [ i ] ;
@@ -190,6 +246,7 @@ export class UriTemplate {
190
246
}
191
247
192
248
match ( uri : string ) : Variables | null {
249
+ UriTemplate . validateLength ( uri , MAX_TEMPLATE_LENGTH , "URI" ) ;
193
250
let pattern = "^" ;
194
251
const names : Array < { name : string ; exploded : boolean } > = [ ] ;
195
252
@@ -206,6 +263,7 @@ export class UriTemplate {
206
263
}
207
264
208
265
pattern += "$" ;
266
+ UriTemplate . validateLength ( pattern , MAX_REGEX_LENGTH , "Generated regex pattern" ) ;
209
267
const regex = new RegExp ( pattern ) ;
210
268
const match = uri . match ( regex ) ;
211
269
0 commit comments