Conversation
bgub
left a comment
There was a problem hiding this comment.
Thanks for the contribution! Custom tag types are a nice extensibility feature. However, there are a few issues that need to be addressed before this can be merged:
Critical
- Code injection vulnerability: The
contentfrom templates is interpolated directly into a single-quoted string in the generated function code (compile-string.ts:128). A template like<%* foo'; malicious(); ' %>would allow arbitrary code execution. The content needs to be escaped or passed by reference rather than inlined. - Unused variable:
customTagis assigned but never used on line 127.
Important
- Type safety regression: Removing
TagTypeand wideningttostringloses type safety for all consumers. Consider usingTagType = "r" | "e" | "i" | "" | (string & {})instead. - No prefix collision validation: Custom tag prefixes could collide with built-in prefixes (
"","=","~") or trim indicators ("-","_"), causing silent bugs. - Breaking change:
customTagsis a required property onEtaConfig, which breaks anyone implementing the interface. Consider making it optional.
Minor
- Code style inconsistencies (missing spaces after
if/else if, missing semicolons). - No tests for the new feature.
- Typo in PR title: "suport" → "support".
Generated by Claude Code
| returnStr += content + "\n"; | ||
|
|
||
| } else if (type in config.customTags) { | ||
| const customTag = config.customTags[type]; |
There was a problem hiding this comment.
Security: Code injection via template content
The content variable is interpolated directly into a single-quoted string literal without any escaping:
returnStr += `__eta.res += this.config.customTags['${type}']('${content}', ${config.varName});\n`;If a template contains something like <%* foo'; console.log('pwned'); ' %>, the single quote in content will break out of the string and allow arbitrary code execution in the compiled template function.
content needs to be properly escaped (at minimum, single quotes and backslashes), or passed through a mechanism that doesn't involve string interpolation into generated code. Consider passing content as a variable reference rather than inlining it as a string literal, similar to how the built-in tag types handle their content.
Generated by Claude Code
| // execute | ||
| returnStr += content + "\n"; | ||
|
|
||
| } else if (type in config.customTags) { |
There was a problem hiding this comment.
| returnStr += content + "\n"; | ||
|
|
||
| } else if (type in config.customTags) { | ||
| const customTag = config.customTags[type]; |
There was a problem hiding this comment.
Also worth noting: there's no validation that type (the custom tag key) doesn't contain characters that would break out of the bracket notation string (e.g., a key containing '). While users control the keys via customTags config, it's still worth sanitizing or validating tag prefix characters.
Generated by Claude Code
|
|
||
| export type TagType = "r" | "e" | "i" | ""; | ||
|
|
||
| export interface TemplateObject { |
There was a problem hiding this comment.
Removing the TagType union and widening t to string is a type safety regression that affects all consumers of TemplateObject (including the plugin API's processAST).
Consider keeping the named type and extending it instead:
export type TagType = "r" | "e" | "i" | "" | (string & {});This preserves autocomplete for the built-in types while still allowing arbitrary custom tag strings.
Generated by Claude Code
| @@ -40,6 +38,8 @@ export function parse(this: Eta, str: string): Array<AstObject> { | |||
| let lastIndex = 0; | |||
| const parseOptions = config.parse; | |||
There was a problem hiding this comment.
There's no validation that custom tag prefixes don't collide with the built-in prefixes ("", "=", "~") or with the trim indicators ("-", "_"). A collision would cause silent, confusing bugs. Consider adding a check here that warns or throws if a custom prefix conflicts with built-in ones.
Generated by Claude Code
| let currentType = ""; | ||
| if(prefix === config.parse.exec) currentType = "e"; | ||
| else if(prefix === config.parse.interpolate) currentType = "i"; | ||
| else if(prefix === config.parse.raw) currentType = "r"; | ||
| // custom tags | ||
| else if(customTagPrefixes.includes(prefix)) currentType = prefix; | ||
|
|
There was a problem hiding this comment.
Nit: missing spaces after if/else if — the rest of the codebase uses if ( with a space. Also missing semicolon on line 39.
| let currentType = ""; | |
| if(prefix === config.parse.exec) currentType = "e"; | |
| else if(prefix === config.parse.interpolate) currentType = "i"; | |
| else if(prefix === config.parse.raw) currentType = "r"; | |
| // custom tags | |
| else if(customTagPrefixes.includes(prefix)) currentType = prefix; | |
| let currentType = ""; | |
| if (prefix === config.parse.exec) currentType = "e"; | |
| else if (prefix === config.parse.interpolate) currentType = "i"; | |
| else if (prefix === config.parse.raw) currentType = "r"; | |
| // custom tags | |
| else if (customTagPrefixes.includes(prefix)) currentType = prefix; |
Generated by Claude Code
| /** Holds cache of resolved filepaths. Set to `false` to disable. */ | ||
| cacheFilepaths: boolean; | ||
|
|
||
| /** Object specifying custom tags. Keys are tag prefixes, values are functions which take tag content and return a string. */ |
There was a problem hiding this comment.
Making customTags a required property on EtaConfig is a breaking change for anyone implementing or extending the interface directly. Consider making it optional (customTags?:) to maintain backwards compatibility.
Generated by Claude Code
Added very basic support for custom tag types.
Example - a "comment" tag
<%# ... %>which does nothing, and a localisation tag<%* ... %>which looks up strings in a dictionary.And then used in HTML:
Outputs: