@@ -169,40 +169,368 @@ abstract class NotYeoman {
169169 }
170170}
171171
172- // Patterns that indicate code execution attempts in EJS tags.
173- // Applied to ALL tag types (<% %>, <%= %>, <%- %>) except comments (<%# %>).
174- const DANGEROUS_PATTERNS = [
175- / \b r e q u i r e \s * \( / ,
176- / \b i m p o r t \s * \( / ,
177- / \b c h i l d _ p r o c e s s \b / ,
178- / \b p r o c e s s \s * \. \s * (?: e n v | e x i t | k i l l | b i n d i n g | d l o p e n | m a i n M o d u l e | g e t B u i l t i n M o d u l e ) / ,
179- / \b g l o b a l \s * \. / ,
180- / \b g l o b a l T h i s \s * \. / ,
181- / \b (?: e v a l | F u n c t i o n ) \s * \( / ,
182- / \b e x e c S y n c \b / ,
183- / \b e x e c \s * \( / ,
184- / \b s p a w n (?: S y n c ) ? \s * \( / ,
185- / \b f s \b \s * \. \s * (?: r e a d | w r i t e | u n l i n k | r m | c h m o d | c h o w n | m k d i r | r e n a m e | s y m l i n k | l i n k ) / ,
186- / \b _ _ d i r n a m e \b / ,
187- / \b _ _ f i l e n a m e \b / ,
188- / \b m o d u l e \s * \. \s * (?: c o n s t r u c t o r | _ c o m p i l e | _ r e s o l v e F i l e n a m e ) / ,
189- / \. c o n s t r u c t o r \s * \. \s * c o n s t r u c t o r \s * \( / ,
190- / \b R e f l e c t \s * \. \s * (?: c o n s t r u c t | a p p l y ) \s * \( / ,
191- ] ;
172+ // Allowlist-based validator for custom EJS templates.
173+ // Instead of trying to block dangerous patterns (infinite bypass surface),
174+ // we only permit the narrow subset of JS that templates legitimately need.
175+
176+ const ALLOWED_EXPRESSION_CALL_TARGETS = new Set ( [
177+ 'replace' ,
178+ 'uuid' ,
179+ 'join' ,
180+ 'toString' ,
181+ 'trim' ,
182+ 'toLowerCase' ,
183+ 'toUpperCase' ,
184+ 'slice' ,
185+ 'substring' ,
186+ 'indexOf' ,
187+ 'includes' ,
188+ 'split' ,
189+ 'concat' ,
190+ 'startsWith' ,
191+ 'endsWith' ,
192+ 'padStart' ,
193+ 'padEnd' ,
194+ ] ) ;
195+
196+ const ALLOWED_SCRIPTLET_METHODS = new Set ( [
197+ 'forEach' ,
198+ 'map' ,
199+ 'filter' ,
200+ 'includes' ,
201+ 'indexOf' ,
202+ 'length' ,
203+ 'push' ,
204+ 'join' ,
205+ 'some' ,
206+ 'every' ,
207+ 'find' ,
208+ 'findIndex' ,
209+ 'slice' ,
210+ 'concat' ,
211+ 'keys' ,
212+ 'values' ,
213+ 'entries' ,
214+ ] ) ;
215+
216+ function extractEjsTags ( template : string ) : { type : string ; code : string } [ ] {
217+ const tags : { type : string ; code : string } [ ] = [ ] ;
218+ let i = 0 ;
219+ while ( i < template . length ) {
220+ const start = template . indexOf ( '<%' , i ) ;
221+ if ( start === - 1 ) {
222+ break ;
223+ }
224+
225+ const afterOpen = start + 2 ;
226+ if ( afterOpen >= template . length ) {
227+ break ;
228+ }
229+
230+ const firstChar = template [ afterOpen ] ;
231+ if ( firstChar === '#' ) {
232+ const end = template . indexOf ( '%>' , afterOpen ) ;
233+ i = end === - 1 ? template . length : end + 2 ;
234+ continue ;
235+ }
236+
237+ let type : string ;
238+ let codeStart : number ;
239+ if ( firstChar === '=' || firstChar === '-' ) {
240+ type = firstChar ;
241+ codeStart = afterOpen + 1 ;
242+ } else {
243+ type = '%' ;
244+ codeStart = afterOpen ;
245+ }
246+
247+ let pos = codeStart ;
248+ let code = '' ;
249+ let found = false ;
250+ while ( pos < template . length ) {
251+ const ch = template [ pos ] ;
252+ if ( ch === "'" || ch === '"' || ch === '`' ) {
253+ const quote = ch ;
254+ pos ++ ;
255+ while ( pos < template . length && template [ pos ] !== quote ) {
256+ if ( template [ pos ] === '\\' ) {
257+ pos ++ ;
258+ }
259+ pos ++ ;
260+ }
261+ pos ++ ;
262+ } else if ( template [ pos ] === '%' && template [ pos + 1 ] === '>' ) {
263+ code = template . slice ( codeStart , pos ) ;
264+ found = true ;
265+ pos += 2 ;
266+ break ;
267+ } else {
268+ pos ++ ;
269+ }
270+ }
271+
272+ if ( ! found ) {
273+ code = template . slice ( codeStart ) ;
274+ pos = template . length ;
275+ }
276+
277+ tags . push ( { type, code : code . trim ( ) } ) ;
278+ i = pos ;
279+ }
280+ return tags ;
281+ }
282+
283+ function isSafeBracketContent ( inner : string ) : boolean {
284+ if ( / ^ \d + $ / . test ( inner ) ) {
285+ return true ;
286+ }
287+ if ( / ^ [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * $ / . test ( inner ) ) {
288+ return true ;
289+ }
290+ const strLitMatch = inner . match ( / ^ ( [ ' " ] ) ( [ a - z A - Z _ $ ] [ \w . ] * ) \1$ / ) ;
291+ if ( strLitMatch ) {
292+ return true ;
293+ }
294+ return false ;
295+ }
296+
297+ function containsDangerousBrackets ( code : string ) : boolean {
298+ const bracketContent = / \[ ( [ ^ \] ] * ) \] / g;
299+ let m ;
300+ while ( ( m = bracketContent . exec ( code ) ) !== null ) {
301+ if ( ! isSafeBracketContent ( m [ 1 ] . trim ( ) ) ) {
302+ return true ;
303+ }
304+ }
305+ return false ;
306+ }
307+
308+ function isSimpleExpression ( code : string ) : boolean {
309+ const blocked =
310+ / \b ( f u n c t i o n | c l a s s | n e w | d e l e t e | t y p e o f | v o i d | t h i s | p r o c e s s | g l o b a l | g l o b a l T h i s | r e q u i r e | i m p o r t | m o d u l e | e v a l | F u n c t i o n | c o n s t r u c t o r | _ _ p r o t o _ _ | p r o t o t y p e | R e f l e c t | P r o x y | O b j e c t \s * \. \s * (?: c r e a t e | a s s i g n | d e f i n e P r o p e r t | g e t O w n P r o p e r t y N a m e s | g e t P r o t o t y p e O f | s e t P r o t o t y p e O f ) | A r r a y \s * \. \s * f r o m | S t r i n g \s * \. \s * f r o m C h a r C o d e | S y m b o l | B u f f e r | s e t T i m e o u t | s e t I n t e r v a l | s e t I m m e d i a t e | c l e a r T i m e o u t | c l e a r I n t e r v a l | q u e u e M i c r o t a s k | P r o m i s e | a s y n c | a w a i t | y i e l d | r e t u r n | t h r o w | t r y | c a t c h | f i n a l l y | w h i l e | f o r | d o | s w i t c h | w i t h ) \b / ;
311+
312+ if ( blocked . test ( code ) ) {
313+ return false ;
314+ }
315+
316+ if ( containsDangerousBrackets ( code ) ) {
317+ return false ;
318+ }
319+
320+ if ( / ` [ ^ ` ] * \$ \{ / . test ( code ) ) {
321+ return false ;
322+ }
323+
324+ if ( / (?< ! [ = ! < > ] ) = (? ! = ) / . test ( code ) ) {
325+ return false ;
326+ }
327+
328+ const callPattern = / \. ( [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * ) \s * \( / g;
329+ let m ;
330+ while ( ( m = callPattern . exec ( code ) ) !== null ) {
331+ if ( ! ALLOWED_EXPRESSION_CALL_TARGETS . has ( m [ 1 ] ) ) {
332+ return false ;
333+ }
334+ }
335+
336+ const bareCalls = / (?< ! [ . \w ] ) ( [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * ) \s * \( / g;
337+ while ( ( m = bareCalls . exec ( code ) ) !== null ) {
338+ if ( ! ALLOWED_EXPRESSION_CALL_TARGETS . has ( m [ 1 ] ) ) {
339+ return false ;
340+ }
341+ }
342+
343+ return true ;
344+ }
345+
346+ function isAllowedScriptlet ( code : string ) : boolean {
347+ const statements = code . split ( / [ ; \n ] / ) . map ( ( s ) => s . trim ( ) ) . filter ( Boolean ) ;
348+
349+ for ( const stmt of statements ) {
350+ if ( ! isAllowedStatement ( stmt ) ) {
351+ return false ;
352+ }
353+ }
354+ return true ;
355+ }
356+
357+ function isAllowedStatement ( stmt : string ) : boolean {
358+ const hardBlocked =
359+ / \b ( p r o c e s s | g l o b a l | g l o b a l T h i s | r e q u i r e | i m p o r t | m o d u l e | e v a l | F u n c t i o n | R e f l e c t | P r o x y | B u f f e r | s e t T i m e o u t | s e t I n t e r v a l | s e t I m m e d i a t e | c l e a r T i m e o u t | c l e a r I n t e r v a l | q u e u e M i c r o t a s k | _ _ d i r n a m e | _ _ f i l e n a m e | c o n s t r u c t o r | _ _ p r o t o _ _ | p r o t o t y p e | w i t h | y i e l d | a s y n c | a w a i t ) \b / ;
360+ if ( hardBlocked . test ( stmt ) ) {
361+ return false ;
362+ }
363+
364+ if ( / ` [ ^ ` ] * \$ \{ / . test ( stmt ) ) {
365+ return false ;
366+ }
367+
368+ if ( containsDangerousBrackets ( stmt ) ) {
369+ return false ;
370+ }
371+
372+ if ( / ^ \} ? \s * \) ? \s * ; ? \s * \} ? \s * ; ? $ / . test ( stmt ) ) {
373+ return true ;
374+ }
375+
376+ if ( stmt === '{' ) {
377+ return true ;
378+ }
379+
380+ if ( / ^ (?: e l s e \s + ) ? i f \s * \( / . test ( stmt ) ) {
381+ return isSimpleCondition ( stmt ) ;
382+ }
383+ if ( / ^ } \s * e l s e \s * \{ ? $ / . test ( stmt ) || stmt === 'else {' || stmt === 'else' ) {
384+ return true ;
385+ }
386+
387+ if ( / ^ f o r \s * \( / . test ( stmt ) ) {
388+ return isSimpleForLoop ( stmt ) ;
389+ }
390+
391+ if ( / ^ \w [ \w . ] * \s * \. \s * ( f o r E a c h | m a p | f i l t e r | s o m e | e v e r y | f i n d | f i n d I n d e x ) \s * \( / . test ( stmt ) ) {
392+ return isSimpleIterator ( stmt ) ;
393+ }
394+
395+ if ( / ^ (?: c o n s t | l e t | v a r ) \s + / . test ( stmt ) ) {
396+ return isSimpleDeclaration ( stmt ) ;
397+ }
398+
399+ if ( / ^ [ a - z A - Z _ $ ] [ \w . ] * \s * \. \s * ( p u s h | p o p | s h i f t | u n s h i f t ) \s * \( / . test ( stmt ) ) {
400+ return isSimpleMethodCall ( stmt ) ;
401+ }
402+
403+ return false ;
404+ }
405+
406+ function isSimpleCondition ( stmt : string ) : boolean {
407+ const condMatch = stmt . match ( / ^ (?: e l s e \s + ) ? i f \s * \( ( [ \s \S ] * ) \) \s * \{ ? \s * $ / ) ;
408+ if ( ! condMatch ) {
409+ return false ;
410+ }
411+ const cond = condMatch [ 1 ] ;
412+ const hardBlocked =
413+ / \b ( p r o c e s s | g l o b a l | g l o b a l T h i s | r e q u i r e | i m p o r t | m o d u l e | e v a l | F u n c t i o n | R e f l e c t | P r o x y | c o n s t r u c t o r | _ _ p r o t o _ _ ) \b / ;
414+ if ( hardBlocked . test ( cond ) ) {
415+ return false ;
416+ }
417+ if ( / ` [ ^ ` ] * \$ \{ / . test ( cond ) ) {
418+ return false ;
419+ }
420+ const callPattern = / \. ( [ a - z A - Z _ $ ] \w * ) \s * \( / g;
421+ let m ;
422+ while ( ( m = callPattern . exec ( cond ) ) !== null ) {
423+ if ( ! ALLOWED_SCRIPTLET_METHODS . has ( m [ 1 ] ) ) {
424+ return false ;
425+ }
426+ }
427+ if ( containsDangerousBrackets ( cond ) ) {
428+ return false ;
429+ }
430+ return true ;
431+ }
432+
433+ function isSimpleForLoop ( stmt : string ) : boolean {
434+ if ( / ^ f o r \s * \( \s * (?: c o n s t | l e t | v a r ) \s + \[ ? \s * \w + (?: \s * , \s * \w + ) * \s * \] ? \s + (?: o f | i n ) \s + / . test ( stmt ) ) {
435+ const hardBlocked = / \b ( p r o c e s s | g l o b a l | g l o b a l T h i s | r e q u i r e | i m p o r t | m o d u l e | e v a l | F u n c t i o n | R e f l e c t | P r o x y | c o n s t r u c t o r | _ _ p r o t o _ _ ) \b / ;
436+ return ! hardBlocked . test ( stmt ) ;
437+ }
438+ return false ;
439+ }
440+
441+ function isSimpleIterator ( stmt : string ) : boolean {
442+ const hardBlocked =
443+ / \b ( p r o c e s s | g l o b a l | g l o b a l T h i s | r e q u i r e | i m p o r t | m o d u l e | e v a l | F u n c t i o n | R e f l e c t | P r o x y | c o n s t r u c t o r | _ _ p r o t o _ _ ) \b / ;
444+ if ( hardBlocked . test ( stmt ) ) {
445+ return false ;
446+ }
447+ if ( / ` [ ^ ` ] * \$ \{ / . test ( stmt ) ) {
448+ return false ;
449+ }
450+ if ( containsDangerousBrackets ( stmt ) ) {
451+ return false ;
452+ }
453+ return true ;
454+ }
455+
456+ function isSimpleDeclaration ( stmt : string ) : boolean {
457+ const hardBlocked =
458+ / \b ( p r o c e s s | g l o b a l | g l o b a l T h i s | r e q u i r e | i m p o r t | m o d u l e | e v a l | F u n c t i o n | R e f l e c t | P r o x y | c o n s t r u c t o r | _ _ p r o t o _ _ | p r o t o t y p e ) \b / ;
459+ if ( hardBlocked . test ( stmt ) ) {
460+ return false ;
461+ }
462+ if ( / ` [ ^ ` ] * \$ \{ / . test ( stmt ) ) {
463+ return false ;
464+ }
465+ if ( / \b f u n c t i o n \b / . test ( stmt ) ) {
466+ return false ;
467+ }
468+ if ( / = > \s * \{ / . test ( stmt ) ) {
469+ return false ;
470+ }
471+ if ( containsDangerousBrackets ( stmt ) ) {
472+ return false ;
473+ }
474+ return true ;
475+ }
476+
477+ function isSimpleMethodCall ( stmt : string ) : boolean {
478+ const hardBlocked =
479+ / \b ( p r o c e s s | g l o b a l | g l o b a l T h i s | r e q u i r e | i m p o r t | m o d u l e | e v a l | F u n c t i o n | R e f l e c t | P r o x y | c o n s t r u c t o r | _ _ p r o t o _ _ ) \b / ;
480+ if ( hardBlocked . test ( stmt ) ) {
481+ return false ;
482+ }
483+ if ( / ` [ ^ ` ] * \$ \{ / . test ( stmt ) ) {
484+ return false ;
485+ }
486+ if ( containsDangerousBrackets ( stmt ) ) {
487+ return false ;
488+ }
489+ return true ;
490+ }
491+
492+ function normalizeCode ( code : string ) : string {
493+ let result = code ;
494+ // Strip JS comments that could hide content from analysis
495+ result = result . replace ( / \/ \* [ \s \S ] * ?\* \/ / g, ' ' ) ;
496+ result = result . replace ( / \/ \/ [ ^ \n ] * / g, ' ' ) ;
497+ // Resolve string concatenations: 'a' + 'b' -> 'ab'
498+ let prev = '' ;
499+ while ( result !== prev ) {
500+ prev = result ;
501+ result = result . replace ( / ' ( [ ^ ' \\ ] * ) ' \s * \+ \s * ' ( [ ^ ' \\ ] * ) ' / g, "'$1$2'" ) ;
502+ result = result . replace ( / " ( [ ^ " \\ ] * ) " \s * \+ \s * " ( [ ^ " \\ ] * ) " / g, '"$1$2"' ) ;
503+ result = result . replace ( / ' ( [ ^ ' \\ ] * ) ' \s * \+ \s * " ( [ ^ " \\ ] * ) " / g, "'$1$2'" ) ;
504+ result = result . replace ( / " ( [ ^ " \\ ] * ) " \s * \+ \s * ' ( [ ^ ' \\ ] * ) ' / g, '"$1$2"' ) ;
505+ }
506+ // Normalize hex escapes in strings: '\x63' -> 'c'
507+ result = result . replace ( / \\ x ( [ 0 - 9 a - f A - F ] { 2 } ) / g, ( _ , hex ) =>
508+ String . fromCharCode ( parseInt ( hex , 16 ) )
509+ ) ;
510+ // Normalize unicode escapes: '\u0063' -> 'c'
511+ result = result . replace ( / \\ u ( [ 0 - 9 a - f A - F ] { 4 } ) / g, ( _ , hex ) =>
512+ String . fromCharCode ( parseInt ( hex , 16 ) )
513+ ) ;
514+ return result ;
515+ }
192516
193517export function validateCustomTemplate (
194518 templateContent : string ,
195519 templatePath : string
196520) : void {
197- // Match all EJS tags EXCEPT comments (<%# ... %>)
198- const ejsTagRegex = / < % (? ! # ) [ \s \S ] * ?% > / g;
199- let match ;
200- while ( ( match = ejsTagRegex . exec ( templateContent ) ) !== null ) {
201- const code = match [ 0 ] ;
202- for ( const pattern of DANGEROUS_PATTERNS ) {
203- if ( pattern . test ( code ) ) {
521+ const tags = extractEjsTags ( templateContent ) ;
522+ for ( const tag of tags ) {
523+ const code = normalizeCode ( tag . code ) ;
524+ if ( tag . type === '=' || tag . type === '-' ) {
525+ if ( ! isSimpleExpression ( code ) ) {
526+ throw new Error (
527+ `Custom template "${ templatePath } " contains disallowed code in expression tag: ${ tag . code . slice ( 0 , 80 ) } `
528+ ) ;
529+ }
530+ } else {
531+ if ( ! isAllowedScriptlet ( code ) ) {
204532 throw new Error (
205- `Custom template "${ templatePath } " contains disallowed code execution pattern : ${ pattern . source } `
533+ `Custom template "${ templatePath } " contains disallowed code in scriptlet tag : ${ tag . code . slice ( 0 , 80 ) } `
206534 ) ;
207535 }
208536 }
0 commit comments