@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 +29 -17
- package/lib/engine.js +20 -7
- package/lib/index.js +10 -0
- package/lib/ir.js +1 -1
- package/lib/tokenparser.js +5 -5
- package/lib/utils.js +10 -3
- package/package.json +1 -1
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
|
|
395
|
-
//
|
|
396
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
645
|
-
*
|
|
646
|
-
*
|
|
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 (
|
|
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
|
|
package/lib/tokenparser.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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 {
|