@hostwebhook/template-engine 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -84,6 +84,12 @@ declare function resolveInput(value: string | undefined | null, payload: unknown
84
84
  */
85
85
  declare function resolveFieldPath(field: string): string;
86
86
 
87
+ /**
88
+ * Validate code/expression against blocked patterns.
89
+ * Throws if any dangerous pattern is detected.
90
+ */
91
+ declare function validateCode(code: string): void;
92
+
87
93
  /**
88
94
  * Resolve a dot-notation path against a TemplateContext.
89
95
  * Supports bracket notation for array indexing: `payload.items[0].name`
@@ -116,4 +122,4 @@ declare function applyPipe(pipe: string, val: unknown, arg?: string, ctx?: Templ
116
122
  */
117
123
  declare function evaluate(expr: string, ctx: TemplateContext, opts?: TemplateOptions): string;
118
124
 
119
- export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolveFieldPath, resolveInput, resolvePath, tryNullCoalesce, tryTernary, valueToString };
125
+ export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolveFieldPath, resolveInput, resolvePath, tryNullCoalesce, tryTernary, validateCode, valueToString };
package/dist/index.d.ts CHANGED
@@ -84,6 +84,12 @@ declare function resolveInput(value: string | undefined | null, payload: unknown
84
84
  */
85
85
  declare function resolveFieldPath(field: string): string;
86
86
 
87
+ /**
88
+ * Validate code/expression against blocked patterns.
89
+ * Throws if any dangerous pattern is detected.
90
+ */
91
+ declare function validateCode(code: string): void;
92
+
87
93
  /**
88
94
  * Resolve a dot-notation path against a TemplateContext.
89
95
  * Supports bracket notation for array indexing: `payload.items[0].name`
@@ -116,4 +122,4 @@ declare function applyPipe(pipe: string, val: unknown, arg?: string, ctx?: Templ
116
122
  */
117
123
  declare function evaluate(expr: string, ctx: TemplateContext, opts?: TemplateOptions): string;
118
124
 
119
- export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolveFieldPath, resolveInput, resolvePath, tryNullCoalesce, tryTernary, valueToString };
125
+ export { type CodeResult, type TemplateContext, type TemplateMode, type TemplateOptions, type TemplateResult, applyPipe, evaluate, renderTemplate, resolveFieldPath, resolveInput, resolvePath, tryNullCoalesce, tryTernary, validateCode, valueToString };
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  resolvePath: () => resolvePath,
39
39
  tryNullCoalesce: () => tryNullCoalesce,
40
40
  tryTernary: () => tryTernary,
41
+ validateCode: () => validateCode,
41
42
  valueToString: () => valueToString
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
@@ -90,6 +91,72 @@ function valueToString(val) {
90
91
 
91
92
  // src/sandbox.ts
92
93
  var vm = __toESM(require("vm"));
94
+ var BLOCKED_PATTERNS = [
95
+ /\bconstructor\b/,
96
+ // this.constructor.constructor('return process')()
97
+ /\b__proto__\b/,
98
+ // prototype chain traversal
99
+ /\bprototype\b/,
100
+ // prototype manipulation
101
+ /\bprocess\b/,
102
+ // Node.js process object
103
+ /\brequire\s*\(/,
104
+ // CommonJS require
105
+ /\bimport\s*\(/,
106
+ // Dynamic import
107
+ /\bglobal\b/,
108
+ // global object
109
+ /\bglobalThis\b/,
110
+ // globalThis reference
111
+ /\beval\s*\(/,
112
+ // eval()
113
+ /\bFunction\s*\(/,
114
+ // new Function()
115
+ /\bProxy\s*\(/,
116
+ // Proxy constructor
117
+ /\bReflect\b/,
118
+ // Reflect API
119
+ /\bBuffer\b/,
120
+ // Node.js Buffer
121
+ /\bSharedArrayBuffer\b/,
122
+ // SharedArrayBuffer
123
+ /\bAtomics\b/,
124
+ // Atomics API
125
+ /\bWebAssembly\b/,
126
+ // WebAssembly
127
+ /\b__defineGetter__\b/,
128
+ // Legacy property definition
129
+ /\b__defineSetter__\b/,
130
+ // Legacy property definition
131
+ /\b__lookupGetter__\b/,
132
+ // Legacy property lookup
133
+ /\b__lookupSetter__\b/
134
+ // Legacy property lookup
135
+ ];
136
+ function validateCode(code) {
137
+ for (const pattern of BLOCKED_PATTERNS) {
138
+ if (pattern.test(code)) {
139
+ throw new Error(`Blocked: unsafe pattern "${pattern.source}" detected`);
140
+ }
141
+ }
142
+ }
143
+ function deepFreeze(obj, seen = /* @__PURE__ */ new WeakSet()) {
144
+ if (obj === null || obj === void 0) return;
145
+ if (typeof obj !== "object" && typeof obj !== "function") return;
146
+ if (seen.has(obj)) return;
147
+ seen.add(obj);
148
+ try {
149
+ Object.freeze(obj);
150
+ for (const key of Object.getOwnPropertyNames(obj)) {
151
+ try {
152
+ const val = obj[key];
153
+ deepFreeze(val, seen);
154
+ } catch {
155
+ }
156
+ }
157
+ } catch {
158
+ }
159
+ }
93
160
  function buildNodeLookup(workspacePayloads) {
94
161
  return (nodeName) => {
95
162
  if (!workspacePayloads) throw new Error("$() requires workspace context");
@@ -120,16 +187,23 @@ function buildSandbox(ctx, opts, logs) {
120
187
  }
121
188
  return String(v);
122
189
  };
190
+ let safePayload;
191
+ try {
192
+ safePayload = structuredClone(ctx.payload);
193
+ } catch {
194
+ safePayload = JSON.parse(JSON.stringify(ctx.payload ?? null));
195
+ }
123
196
  const sandbox = {
124
- payload: ctx.payload,
197
+ payload: safePayload,
125
198
  $: buildNodeLookup(opts.workspacePayloads),
126
- headers: ctx.headers,
127
- meta: ctx.meta,
128
- JSON,
199
+ headers: { ...ctx.headers ?? {} },
200
+ meta: { ...ctx.meta ?? {} },
201
+ // Safe builtins only — no process, require, import, eval, Function, Proxy, Reflect
202
+ JSON: { parse: JSON.parse, stringify: JSON.stringify },
129
203
  Math,
130
204
  Date,
131
- Array,
132
- Object,
205
+ Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
206
+ Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, freeze: Object.freeze },
133
207
  String,
134
208
  Number,
135
209
  Boolean,
@@ -141,7 +215,8 @@ function buildSandbox(ctx, opts, logs) {
141
215
  encodeURIComponent,
142
216
  decodeURIComponent,
143
217
  Map,
144
- Set
218
+ Set,
219
+ undefined: void 0
145
220
  };
146
221
  if (opts.mode === "code" && logs) {
147
222
  sandbox.console = {
@@ -150,9 +225,21 @@ function buildSandbox(ctx, opts, logs) {
150
225
  error: (...args) => logs.push(`[error] ${args.map(stringify).join(" ")}`)
151
226
  };
152
227
  }
228
+ for (const key of Object.keys(sandbox)) {
229
+ const val = sandbox[key];
230
+ if (typeof val === "object" && val !== null && key !== "payload") {
231
+ deepFreeze(val);
232
+ }
233
+ }
153
234
  return vm.createContext(sandbox);
154
235
  }
155
236
  function evalJsExpression(expr, ctx, opts) {
237
+ try {
238
+ validateCode(expr);
239
+ } catch (err) {
240
+ console.error(`[template-engine] ${err instanceof Error ? err.message : err}: "${expr.slice(0, 80)}"`);
241
+ return void 0;
242
+ }
156
243
  const context = buildSandbox(ctx, { ...opts, mode: "text" });
157
244
  try {
158
245
  const script = new vm.Script(`(${expr})`, { filename: "template-expr.js" });
@@ -446,6 +533,12 @@ var vm2 = __toESM(require("vm"));
446
533
  function executeCodeTemplate(code, ctx, opts) {
447
534
  const logs = [];
448
535
  const timeout = opts.timeout ?? 5e3;
536
+ try {
537
+ validateCode(code);
538
+ } catch (err) {
539
+ const message = err instanceof Error ? err.message : String(err);
540
+ return { output: null, statusCode: 500, logs, error: message };
541
+ }
449
542
  let sandboxPayload = ctx.payload;
450
543
  if (opts.batchMode) {
451
544
  const meta = ctx.payload?._meta;
@@ -565,5 +658,6 @@ function resolveFieldPath(field) {
565
658
  resolvePath,
566
659
  tryNullCoalesce,
567
660
  tryTernary,
661
+ validateCode,
568
662
  valueToString
569
663
  });
package/dist/index.mjs CHANGED
@@ -46,6 +46,72 @@ function valueToString(val) {
46
46
 
47
47
  // src/sandbox.ts
48
48
  import * as vm from "vm";
49
+ var BLOCKED_PATTERNS = [
50
+ /\bconstructor\b/,
51
+ // this.constructor.constructor('return process')()
52
+ /\b__proto__\b/,
53
+ // prototype chain traversal
54
+ /\bprototype\b/,
55
+ // prototype manipulation
56
+ /\bprocess\b/,
57
+ // Node.js process object
58
+ /\brequire\s*\(/,
59
+ // CommonJS require
60
+ /\bimport\s*\(/,
61
+ // Dynamic import
62
+ /\bglobal\b/,
63
+ // global object
64
+ /\bglobalThis\b/,
65
+ // globalThis reference
66
+ /\beval\s*\(/,
67
+ // eval()
68
+ /\bFunction\s*\(/,
69
+ // new Function()
70
+ /\bProxy\s*\(/,
71
+ // Proxy constructor
72
+ /\bReflect\b/,
73
+ // Reflect API
74
+ /\bBuffer\b/,
75
+ // Node.js Buffer
76
+ /\bSharedArrayBuffer\b/,
77
+ // SharedArrayBuffer
78
+ /\bAtomics\b/,
79
+ // Atomics API
80
+ /\bWebAssembly\b/,
81
+ // WebAssembly
82
+ /\b__defineGetter__\b/,
83
+ // Legacy property definition
84
+ /\b__defineSetter__\b/,
85
+ // Legacy property definition
86
+ /\b__lookupGetter__\b/,
87
+ // Legacy property lookup
88
+ /\b__lookupSetter__\b/
89
+ // Legacy property lookup
90
+ ];
91
+ function validateCode(code) {
92
+ for (const pattern of BLOCKED_PATTERNS) {
93
+ if (pattern.test(code)) {
94
+ throw new Error(`Blocked: unsafe pattern "${pattern.source}" detected`);
95
+ }
96
+ }
97
+ }
98
+ function deepFreeze(obj, seen = /* @__PURE__ */ new WeakSet()) {
99
+ if (obj === null || obj === void 0) return;
100
+ if (typeof obj !== "object" && typeof obj !== "function") return;
101
+ if (seen.has(obj)) return;
102
+ seen.add(obj);
103
+ try {
104
+ Object.freeze(obj);
105
+ for (const key of Object.getOwnPropertyNames(obj)) {
106
+ try {
107
+ const val = obj[key];
108
+ deepFreeze(val, seen);
109
+ } catch {
110
+ }
111
+ }
112
+ } catch {
113
+ }
114
+ }
49
115
  function buildNodeLookup(workspacePayloads) {
50
116
  return (nodeName) => {
51
117
  if (!workspacePayloads) throw new Error("$() requires workspace context");
@@ -76,16 +142,23 @@ function buildSandbox(ctx, opts, logs) {
76
142
  }
77
143
  return String(v);
78
144
  };
145
+ let safePayload;
146
+ try {
147
+ safePayload = structuredClone(ctx.payload);
148
+ } catch {
149
+ safePayload = JSON.parse(JSON.stringify(ctx.payload ?? null));
150
+ }
79
151
  const sandbox = {
80
- payload: ctx.payload,
152
+ payload: safePayload,
81
153
  $: buildNodeLookup(opts.workspacePayloads),
82
- headers: ctx.headers,
83
- meta: ctx.meta,
84
- JSON,
154
+ headers: { ...ctx.headers ?? {} },
155
+ meta: { ...ctx.meta ?? {} },
156
+ // Safe builtins only — no process, require, import, eval, Function, Proxy, Reflect
157
+ JSON: { parse: JSON.parse, stringify: JSON.stringify },
85
158
  Math,
86
159
  Date,
87
- Array,
88
- Object,
160
+ Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
161
+ Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, freeze: Object.freeze },
89
162
  String,
90
163
  Number,
91
164
  Boolean,
@@ -97,7 +170,8 @@ function buildSandbox(ctx, opts, logs) {
97
170
  encodeURIComponent,
98
171
  decodeURIComponent,
99
172
  Map,
100
- Set
173
+ Set,
174
+ undefined: void 0
101
175
  };
102
176
  if (opts.mode === "code" && logs) {
103
177
  sandbox.console = {
@@ -106,9 +180,21 @@ function buildSandbox(ctx, opts, logs) {
106
180
  error: (...args) => logs.push(`[error] ${args.map(stringify).join(" ")}`)
107
181
  };
108
182
  }
183
+ for (const key of Object.keys(sandbox)) {
184
+ const val = sandbox[key];
185
+ if (typeof val === "object" && val !== null && key !== "payload") {
186
+ deepFreeze(val);
187
+ }
188
+ }
109
189
  return vm.createContext(sandbox);
110
190
  }
111
191
  function evalJsExpression(expr, ctx, opts) {
192
+ try {
193
+ validateCode(expr);
194
+ } catch (err) {
195
+ console.error(`[template-engine] ${err instanceof Error ? err.message : err}: "${expr.slice(0, 80)}"`);
196
+ return void 0;
197
+ }
112
198
  const context = buildSandbox(ctx, { ...opts, mode: "text" });
113
199
  try {
114
200
  const script = new vm.Script(`(${expr})`, { filename: "template-expr.js" });
@@ -402,6 +488,12 @@ import * as vm2 from "vm";
402
488
  function executeCodeTemplate(code, ctx, opts) {
403
489
  const logs = [];
404
490
  const timeout = opts.timeout ?? 5e3;
491
+ try {
492
+ validateCode(code);
493
+ } catch (err) {
494
+ const message = err instanceof Error ? err.message : String(err);
495
+ return { output: null, statusCode: 500, logs, error: message };
496
+ }
405
497
  let sandboxPayload = ctx.payload;
406
498
  if (opts.batchMode) {
407
499
  const meta = ctx.payload?._meta;
@@ -520,5 +612,6 @@ export {
520
612
  resolvePath,
521
613
  tryNullCoalesce,
522
614
  tryTernary,
615
+ validateCode,
523
616
  valueToString
524
617
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/template-engine",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Template interpolation and sandboxed JS execution engine for HostWebhook",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",