Repository: jsonata-js/jsonata
Pull request #491 (2020-11-16 23:45:53)

feat(regex): Add support for pluggable regex engines for use in evaluation
*Motivation:*
I would like to use JSONata with [node-re2](https://github.com/uhop/node-re2) - a regex node.js implementation that guards against backtracking. 
This - in combination with work like https://github.com/jsonata-js/jsonata/pull/478 - will make it easier for me to run user-supplied JSONata in a sandbox. 

The specific attack I'm looking to guard against is an [ReDoS](https://nodejs.org/en/docs/guides/dont-block-the-event-loop/#blocking-the-event-loop-redos)

coveralls: 
[![Coverage Status](https://coveralls.io/builds/35012126/badge)](https://coveralls.io/builds/35012126)

Coverage remained the same at 100.0% when pulling **b2ccd759dcdba9c532bd33a4fa1a7f3ff647dac4 on maxwellgerber:feat/pluggable-regex** into **68346a6d52ac1367258d6c66bc120b4c44935d03 on jsonata-js:master**.


Patch:
diff --git a/src/jsonata.js b/src/jsonata.js
index bfc8db9..a91eb0f 100644
--- a/src/jsonata.js
+++ b/src/jsonata.js
@@ -1088,7 +1088,7 @@ var jsonata = (function() {
      * @returns {Function} Higher order function representing prepared regex
      */
     function evaluateRegex(expr) {
-        var re = new RegExp(expr.value);
+        var re = new jsonata.RegexEngine(expr.value);
         var closure = function(str, fromIndex) {
             var result;
             re.lastIndex = fromIndex || 0;
@@ -2041,7 +2041,9 @@ var jsonata = (function() {
     /**
      * JSONata
      * @param {Object} expr - JSONata expression
-     * @param {boolean} options - recover: attempt to recover on parse error
+     * @param {Object} options
+     * @param {boolean} options.recover: attempt to recover on parse error
+     * @param {Function} options.RegexEngine: RegEx class constructor to use
      * @returns {{evaluate: evaluate, assign: assign}} Evaluated expression
      */
     function jsonata(expr, options) {
@@ -2066,6 +2068,12 @@ var jsonata = (function() {
             return timestamp.getTime();
         }, '<:n>'));
 
+        if(options && options.RegexEngine) {
+            jsonata.RegexEngine = options.RegexEngine;
+        } else {
+            jsonata.RegexEngine = RegExp;
+        }
+
         return {
             evaluate: function (input, bindings, callback) {
                 // throw if the expression compiled with syntax errors


Test patch:
diff --git a/test/parser-pluggable-regex.js b/test/parser-pluggable-regex.js
new file mode 100644
index 0000000..1ba6029
--- /dev/null
+++ b/test/parser-pluggable-regex.js
@@ -0,0 +1,28 @@
+"use strict";
+
+var jsonata = require('../src/jsonata');
+var assert = require('assert');
+var chai = require("chai");
+var expect = chai.expect;
+
+describe('Invoke parser with custom RegexEngine param', function() {
+
+    var regexContentSpy = null;
+    var regexEvalSpy = null;
+
+    function RegexEngineSpy(content) {
+        regexContentSpy = content;
+
+        this.exec = function(input) {
+            regexEvalSpy = input;
+            return null;
+        }
+    }
+
+    it('should call RegexEngine param constructure during evaluation', function() {
+        var expr = jsonata('$replace(\"foo\", /bar/, \"baaz\")', { RegexEngine: RegexEngineSpy });
+        expr.evaluate()
+        assert.deepEqual(regexContentSpy.toString(), "/bar/g");
+        assert.deepEqual(regexEvalSpy, "foo");
+    });
+});