@rhinostone/swig-core 2.8.1 → 2.8.3

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/lib/backend.js CHANGED
@@ -391,9 +391,10 @@ exports.compile = function (template, parents, options, blockName) {
391
391
  // else verbatim string fallback preserves the userland setTag
392
392
  // path (compile functions that still hand in raw JS-source
393
393
  // fragments). `resolveFrom` is a plain filesystem path that must
394
- // be JSON-escaped into a string literal — the frontend's
395
- // include-tag parse handler has already applied a `\\` → `\\\\`
396
- // backslash escape before handing it off. `ignoreMissing` wraps
394
+ // be escaped into a string literal — escapeTextValue below (backslash,
395
+ // newline, double-quote) is the single chokepoint that does it for all
396
+ // five node types, every flavor and userland setTag, so no unescaped
397
+ // filename can break out of the literal (GHSA-q89c). `ignoreMissing` wraps
397
398
  // the emission in `try { ... } catch (e) {}` so missing-file
398
399
  // errors collapse to the empty string.
399
400
  var incPathJS, incCtxJS;
@@ -419,7 +420,7 @@ exports.compile = function (template, parents, options, blockName) {
419
420
  }
420
421
  out += (node.ignoreMissing ? ' try {\n' : '') +
421
422
  '_output += _swig.compileFile(' + incPathJS + ', {' +
422
- 'resolveFrom: "' + node.resolveFrom + '"' +
423
+ 'resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"' +
423
424
  '})(' + incSelector + ');\n' +
424
425
  (node.ignoreMissing ? '} catch (e) {}\n' : '');
425
426
  return;
@@ -453,7 +454,7 @@ exports.compile = function (template, parents, options, blockName) {
453
454
  } else {
454
455
  incdSelector = '_utils.extend({}, _ctx, ' + incdCtxJS + ')';
455
456
  }
456
- var incdOpts = '{resolveFrom: "' + (node.resolveFrom || '') + '"}';
457
+ var incdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"}';
457
458
  out += (node.ignoreMissing ? ' try {\n' : '') +
458
459
  '_output += (await (await _swig.getTemplate(' + incdPathJS + ', ' + incdOpts + '))(' + incdSelector + ')).output;\n' +
459
460
  (node.ignoreMissing ? '} catch (e) {}\n' : '');
@@ -475,7 +476,7 @@ exports.compile = function (template, parents, options, blockName) {
475
476
  } else {
476
477
  impdPathJS = node.path;
477
478
  }
478
- var impdOpts = '{resolveFrom: "' + (node.resolveFrom || '') + '"}';
479
+ var impdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"}';
479
480
  out += '_ctx.' + node.alias + ' = (await (await _swig.getTemplate(' + impdPathJS + ', ' + impdOpts + '))(_ctx)).exports;\n';
480
481
  return;
481
482
  }
@@ -500,7 +501,7 @@ exports.compile = function (template, parents, options, blockName) {
500
501
  } else {
501
502
  fromdPathJS = node.path;
502
503
  }
503
- var fromdOpts = '{resolveFrom: "' + (node.resolveFrom || '') + '"}';
504
+ var fromdOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"}';
504
505
  var fromdBindings = '';
505
506
  utils.each(fromdImports, function (entry) {
506
507
  var bindName = entry.alias || entry.name;
@@ -569,7 +570,7 @@ exports.compile = function (template, parents, options, blockName) {
569
570
  } else {
570
571
  extPathJS = node.path;
571
572
  }
572
- var extOpts = '{resolveFrom: "' + (node.resolveFrom || '') + '"}';
573
+ var extOpts = '{resolveFrom: "' + escapeTextValue(node.resolveFrom || '') + '"}';
573
574
  out += 'var _parentTpl = await _swig.getTemplate(' + extPathJS + ', ' + extOpts + ');\n';
574
575
  out += 'var _parentResult = await _parentTpl(_ctx, _mergedBlocks);\n';
575
576
  out += '_output = _parentResult.output;\n';
@@ -762,7 +763,7 @@ function checkDangerousSegment(segment, d, node) {
762
763
  if (d.dangerousProps.indexOf(segment) !== -1) {
763
764
  var line = (node && node.loc && node.loc.line) || undefined;
764
765
  var filename = (node && node.loc && node.loc.filename) || undefined;
765
- d.throwError('Unsafe access to "' + segment + '" is not allowed in templates (CVE-2023-25345)', line, filename);
766
+ d.throwError('Unsafe access to "' + segment + '" is not allowed in templates (prototype-chain guard)', line, filename);
766
767
  }
767
768
  }
768
769
 
@@ -880,7 +881,7 @@ function emitAccess(node, d) {
880
881
  if (node.key && node.key.type === 'Literal' && node.key.kind === 'string') {
881
882
  checkDangerousSegment(node.key.value, d, node);
882
883
  }
883
- return emitExpr(node.object, d) + '[' + emitExpr(node.key, d) + ']';
884
+ return groupOperand(node.object, emitExpr(node.object, d), true) + '[' + emitExpr(node.key, d) + ']';
884
885
  }
885
886
 
886
887
  /*!
@@ -893,9 +894,24 @@ function isArithmeticOp(op) {
893
894
  return op === '+' || op === '-' || op === '*' || op === '/' || op === '%';
894
895
  }
895
896
 
897
+ /*!
898
+ * Parenthesize an operand whose emitted JS could regroup. The IR tree
899
+ * carries the grouping (`(a + b) * c` parses to a `*` node whose left
900
+ * operand is the `+` node), so a nested binary node is wrapped or JS
901
+ * precedence and associativity regroup it (`a + b * c`). `unary` also
902
+ * wraps a unary operand, for the positions where its prefix would bind
903
+ * differently (`-(-x)`, `(-x)[0]`). @private
904
+ */
905
+ function groupOperand(node, js, unary) {
906
+ if (node && (node.type === 'BinaryOp' || (unary && node.type === 'UnaryOp'))) {
907
+ return '(' + js + ')';
908
+ }
909
+ return js;
910
+ }
911
+
896
912
  function emitBinaryOp(node, d) {
897
- var left = emitExpr(node.left, d),
898
- right = emitExpr(node.right, d);
913
+ var left = groupOperand(node.left, emitExpr(node.left, d)),
914
+ right = groupOperand(node.right, emitExpr(node.right, d));
899
915
  if (isArithmeticOp(node.op)) {
900
916
  return left + ' ' + node.op + ' ' + right;
901
917
  }
@@ -922,11 +938,7 @@ function emitBinaryOp(node, d) {
922
938
  }
923
939
 
924
940
  function emitUnaryOp(node, d) {
925
- var operandJS = emitExpr(node.operand, d);
926
- if (node.operand && node.operand.type === 'BinaryOp') {
927
- operandJS = '(' + operandJS + ')';
928
- }
929
- return node.op + operandJS;
941
+ return node.op + groupOperand(node.operand, emitExpr(node.operand, d), true);
930
942
  }
931
943
 
932
944
  function emitConditional(node, d) {
package/lib/engine.js CHANGED
@@ -21,6 +21,21 @@ var AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
21
21
  */
22
22
  function efn() { return ''; }
23
23
 
24
+ /**
25
+ * Cache key for a compiled template. An async-mode compile returns a
26
+ * different function shape (a Promise of `{ output, exports }`), so it is
27
+ * stored under its own key: neither mode is served, or replaces, the other
28
+ * mode's entry for the same file.
29
+ *
30
+ * @param {string} filename Template identifier (the resolved path for files).
31
+ * @param {object} [options] Compile options; `codegenMode` is read.
32
+ * @return {string} Cache key.
33
+ * @private
34
+ */
35
+ function compileCacheKey(filename, options) {
36
+ return (options && options.codegenMode === 'async') ? filename + '#async' : filename;
37
+ }
38
+
24
39
  /**
25
40
  * Runtime engine plumbing shared across @rhinostone/swig-family frontends.
26
41
  *
@@ -537,7 +552,7 @@ exports.install = function (self, frontend) {
537
552
  };
538
553
 
539
554
  self.compile = function (source, options) {
540
- var key = options ? options.filename : null,
555
+ var key = (options && options.filename) ? compileCacheKey(options.filename, options) : null,
541
556
  cached = key ? cacheGet(key, options) : null,
542
557
  context,
543
558
  contextLength,
@@ -591,7 +606,7 @@ exports.install = function (self, frontend) {
591
606
  if (!options.filename) {
592
607
  options = utils.extend({ filename: pathname }, options);
593
608
  }
594
- cached = cacheGet(pathname, options);
609
+ cached = cacheGet(compileCacheKey(pathname, options), options);
595
610
 
596
611
  if (cached) {
597
612
  if (cb) {
@@ -641,11 +656,9 @@ exports.install = function (self, frontend) {
641
656
  * load a child template at render time.
642
657
  *
643
658
  * Cache semantics — bypasses the cache via `options.cache = false` on
644
- * the inner compile call. The sync compile cache and the async compile
645
- * cache would otherwise share a key (the resolved filename) and serve
646
- * a mode-mismatched compiled fn to the wrong caller. Cleaner
647
- * namespacing (separate sync/async cache buckets, or a key suffix) is
648
- * deferred to a follow-up slice.
659
+ * the inner compile call. `compile` and `compileFile` keep async-mode
660
+ * entries under their own key (see `compileCacheKey`), so a compile in
661
+ * either mode is never served the other mode's function.
649
662
  *
650
663
  * @param {string} pathname Template path; resolved via the active loader.
651
664
  * @param {object} [options] Per-call options. Honored: `resolveFrom`,
package/lib/index.js CHANGED
@@ -7,6 +7,16 @@
7
7
  * @rhinostone/swig.
8
8
  */
9
9
 
10
+ /**
11
+ * Package version number as a string. The packages of the family are
12
+ * released together, so they share one version.
13
+ * @example
14
+ * if (require('@rhinostone/swig-core').version === "2.8.2") { ... }
15
+ *
16
+ * @type {String}
17
+ */
18
+ exports.version = "2.8.3";
19
+
10
20
  exports.utils = require('./utils');
11
21
  exports.security = require('./security');
12
22
  exports.ir = require('./ir');
package/lib/ir.js CHANGED
@@ -153,7 +153,7 @@
153
153
  * @property {IRExpr} [context] Explicit locals for the included template.
154
154
  * @property {boolean} [isolated] Maps to Twig's `only`.
155
155
  * @property {boolean} [ignoreMissing] Swallow loader errors when the file is missing.
156
- * @property {string} [resolveFrom] Including template's filename (backslash-escaped) for loader-relative resolution.
156
+ * @property {string} [resolveFrom] Including template's filename (escaped at emit time) for loader-relative resolution.
157
157
  * @property {IRLoc} [loc]
158
158
  */
159
159
 
@@ -166,7 +166,7 @@ TokenParser.prototype = {
166
166
  if (lastState === _t.BRACKETOPEN) {
167
167
  var strippedMatch = match.replace(/^['"]|['"]$/g, '');
168
168
  if (_dangerousProps.indexOf(strippedMatch) !== -1) {
169
- utils.throwError('Unsafe access to "' + strippedMatch + '" via bracket notation is not allowed in templates (CVE-2023-25345)', self.line, self.filename);
169
+ utils.throwError('Unsafe access to "' + strippedMatch + '" via bracket notation is not allowed in templates (prototype-chain guard)', self.line, self.filename);
170
170
  }
171
171
  }
172
172
  self.filterApplyIdx.push(self.out.length);
@@ -372,7 +372,7 @@ TokenParser.prototype = {
372
372
  }
373
373
  // CVE-2023-25345: block prototype-chain traversal via dot notation
374
374
  if (_dangerousProps.indexOf(match) !== -1) {
375
- utils.throwError('Unsafe access to "' + match + '" is not allowed in templates (CVE-2023-25345)', self.line, self.filename);
375
+ utils.throwError('Unsafe access to "' + match + '" is not allowed in templates (prototype-chain guard)', self.line, self.filename);
376
376
  }
377
377
  self.out.push('.' + match);
378
378
  break;
@@ -404,7 +404,7 @@ TokenParser.prototype = {
404
404
  // CVE-2023-25345: block prototype-chain property access
405
405
  utils.each(match, function (segment) {
406
406
  if (_dangerousProps.indexOf(segment) !== -1) {
407
- utils.throwError('Unsafe access to "' + segment + '" is not allowed in templates (CVE-2023-25345)', self.line, self.filename);
407
+ utils.throwError('Unsafe access to "' + segment + '" is not allowed in templates (prototype-chain guard)', self.line, self.filename);
408
408
  }
409
409
  });
410
410
 
@@ -472,12 +472,12 @@ TokenParser.prototype = {
472
472
  }
473
473
  function guardSegment(segment) {
474
474
  if (_dangerousProps.indexOf(segment) !== -1) {
475
- bail('Unsafe access to "' + segment + '" is not allowed in templates (CVE-2023-25345)');
475
+ bail('Unsafe access to "' + segment + '" is not allowed in templates (prototype-chain guard)');
476
476
  }
477
477
  }
478
478
  function guardBracketString(value) {
479
479
  if (_dangerousProps.indexOf(value) !== -1) {
480
- bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (CVE-2023-25345)');
480
+ bail('Unsafe access to "' + value + '" via bracket notation is not allowed in templates (prototype-chain guard)');
481
481
  }
482
482
  }
483
483
 
package/lib/utils.js CHANGED
@@ -189,9 +189,9 @@ exports.throwError = function (message, line, file) {
189
189
  * Inclusive range generator. Mirrors Twig's `..` operator semantics and
190
190
  * PHP's range(): numeric bounds produce [from, from±1, ..., to]; single-
191
191
  * character string bounds produce a char array across the charCode span.
192
- * Descending when `from > to`. Mismatched or unsupported argument shapes
193
- * return an empty array so compiled templates degrade silently rather
194
- * than throwing at render time.
192
+ * Descending when `from > to`. Mismatched or unsupported argument shapes,
193
+ * and a numeric bound that is not finite, return an empty array so
194
+ * compiled templates degrade silently rather than throwing at render time.
195
195
  *
196
196
  * @param {number|string} from
197
197
  * @param {number|string} to
@@ -200,6 +200,13 @@ exports.throwError = function (message, line, file) {
200
200
  exports.range = function (from, to) {
201
201
  var out = [], i, fc, tc;
202
202
  if (typeof from === 'number' && typeof to === 'number') {
203
+ // An infinite bound is never reached, so the loop below ran until the
204
+ // array overflowed (seconds, and gigabytes, before a RangeError); plain
205
+ // JSON yields Infinity for a number such as 1e999. PHP 8.3's range()
206
+ // rejects such a bound; the range is empty here, as for NaN.
207
+ if (!isFinite(from) || !isFinite(to)) {
208
+ return out;
209
+ }
203
210
  if (from <= to) {
204
211
  for (i = from; i <= to; i += 1) { out.push(i); }
205
212
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-core",
3
- "version": "2.8.1",
3
+ "version": "2.8.3",
4
4
  "description": "Shared IR, backend, and runtime for the @rhinostone/swig family of template engines.",
5
5
  "keywords": [
6
6
  "template",