Skip to content

Commit 9350973

Browse files
fix: validate custom templates for dangerous code execution patterns @W-23595931@
Defense-in-depth: scan EJS scriptlet blocks in custom templates for dangerous patterns (require, import, child_process, eval, etc.) before rendering. Built-in templates are trusted and not scanned. This prevents arbitrary code execution even if a user's global config points to a compromised template directory.
1 parent 7bfcc16 commit 9350973

3 files changed

Lines changed: 126 additions & 0 deletions

File tree

src/generators/baseGenerator.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,42 @@ abstract class NotYeoman {
169169
}
170170
}
171171

172+
// Patterns that indicate code execution attempts in EJS scriptlet blocks
173+
const DANGEROUS_PATTERNS = [
174+
/\brequire\s*\(/,
175+
/\bimport\s*\(/,
176+
/\bchild_process\b/,
177+
/\bprocess\s*\.\s*(?:env|exit|kill|binding|dlopen|mainModule)/,
178+
/\bglobal\s*\./,
179+
/\bglobalThis\s*\./,
180+
/\b(?:eval|Function)\s*\(/,
181+
/\bexecSync\b/,
182+
/\bexec\s*\(/,
183+
/\bspawn(?:Sync)?\s*\(/,
184+
/\bfs\b\s*\.\s*(?:read|write|unlink|rm|chmod|chown|mkdir|rename|symlink|link)/,
185+
/\b__dirname\b/,
186+
/\b__filename\b/,
187+
/\bmodule\s*\.\s*(?:constructor|_compile|_resolveFilename)/,
188+
];
189+
190+
export function validateCustomTemplate(
191+
templateContent: string,
192+
templatePath: string
193+
): void {
194+
const scriptletRegex = /<%(?![-=#])[\s\S]*?%>/g;
195+
let match;
196+
while ((match = scriptletRegex.exec(templateContent)) !== null) {
197+
const code = match[0];
198+
for (const pattern of DANGEROUS_PATTERNS) {
199+
if (pattern.test(code)) {
200+
throw new Error(
201+
`Custom template "${templatePath}" contains disallowed code execution pattern: ${pattern.source}`
202+
);
203+
}
204+
}
205+
}
206+
}
207+
172208
export abstract class BaseGenerator<
173209
TOptions extends TemplateOptions
174210
> extends NotYeoman {
@@ -237,6 +273,18 @@ export abstract class BaseGenerator<
237273
}
238274
}
239275

276+
public async render(
277+
source: string,
278+
destination: string,
279+
data?: Record<string, unknown>
280+
): Promise<void> {
281+
if (this.customTemplatesRootPath && source.startsWith(this.customTemplatesRootPath)) {
282+
const template = await this._fs.promises.readFile(source, 'utf8');
283+
validateCustomTemplate(template, source);
284+
}
285+
return super.render(source, destination, data);
286+
}
287+
240288
public async run(opts?: {
241289
cwd?: string;
242290
customTemplatesRootPathOrGitRepo?: string;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { TemplateService } from './service/templateService';
99
export { TemplateService };
10+
export { validateCustomTemplate } from './generators/baseGenerator';
1011
export * from './utils/types';
1112
export * from './utils/createUtil';
1213
export * from './utils/uiEmbedding';

test/generators/baseGenerator.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { TemplateOptions } from '../../src';
1111
import {
1212
BaseGenerator,
1313
getDefaultApiVersion,
14+
validateCustomTemplate,
1415
} from '../../src/generators/baseGenerator';
1516

1617
describe('BaseGenerator', () => {
@@ -60,3 +61,79 @@ describe('BaseGenerator', () => {
6061
expect(validateOptionsStub.calledOnce).to.be.true;
6162
});
6263
});
64+
65+
describe('validateCustomTemplate', () => {
66+
it('should allow safe interpolation-only templates', () => {
67+
const template = 'public class <%= apiName %> {\n}';
68+
expect(() => validateCustomTemplate(template, 'test.cls')).to.not.throw();
69+
});
70+
71+
it('should allow safe control flow in scriptlet tags', () => {
72+
const template = '<% if (primaryField) { %>\n<%= primaryField %>\n<% } %>';
73+
expect(() => validateCustomTemplate(template, 'test.xml')).to.not.throw();
74+
});
75+
76+
it('should allow forEach loops', () => {
77+
const template = '<% fields.forEach((field) => { %>\n<%= field %>\n<% }); %>';
78+
expect(() => validateCustomTemplate(template, 'test.xml')).to.not.throw();
79+
});
80+
81+
it('should block require() calls', () => {
82+
const template = '<% require("child_process").execSync("calc") %>';
83+
expect(() => validateCustomTemplate(template, 'malicious.cls')).to.throw(
84+
'disallowed code execution pattern'
85+
);
86+
});
87+
88+
it('should block dynamic import()', () => {
89+
const template = '<% const m = await import("fs") %>';
90+
expect(() => validateCustomTemplate(template, 'malicious.cls')).to.throw(
91+
'disallowed code execution pattern'
92+
);
93+
});
94+
95+
it('should block process.env access', () => {
96+
const template = '<% process.env.SECRET %>';
97+
expect(() => validateCustomTemplate(template, 'malicious.cls')).to.throw(
98+
'disallowed code execution pattern'
99+
);
100+
});
101+
102+
it('should block eval()', () => {
103+
const template = '<% eval("malicious code") %>';
104+
expect(() => validateCustomTemplate(template, 'malicious.cls')).to.throw(
105+
'disallowed code execution pattern'
106+
);
107+
});
108+
109+
it('should block Function constructor', () => {
110+
const template = '<% Function("return process")() %>';
111+
expect(() => validateCustomTemplate(template, 'malicious.cls')).to.throw(
112+
'disallowed code execution pattern'
113+
);
114+
});
115+
116+
it('should block execSync', () => {
117+
const template = '<% execSync("whoami") %>';
118+
expect(() => validateCustomTemplate(template, 'malicious.cls')).to.throw(
119+
'disallowed code execution pattern'
120+
);
121+
});
122+
123+
it('should block globalThis access', () => {
124+
const template = '<% globalThis.process %>';
125+
expect(() => validateCustomTemplate(template, 'malicious.cls')).to.throw(
126+
'disallowed code execution pattern'
127+
);
128+
});
129+
130+
it('should not flag output tags containing dangerous-looking strings', () => {
131+
const template = '<%= "require something" %>';
132+
expect(() => validateCustomTemplate(template, 'test.cls')).to.not.throw();
133+
});
134+
135+
it('should not flag comment tags', () => {
136+
const template = '<%# require("fs") %>';
137+
expect(() => validateCustomTemplate(template, 'test.cls')).to.not.throw();
138+
});
139+
});

0 commit comments

Comments
 (0)