2

I am in the process of the strengthening the Content Security Policy for my site. I still have unsafe-eval included because there is one application I developed some time ago that uses it, but no other page does. I've tried modifying it using some libraries to get rid of it, but the program fails in some way when I try that.

I use eval in two places like this:

function derivative(x,y) {
    var fxn = document.getElementById('deriv').value;
    var result = eval(fxn);
    return result;
}
function origfunc(x) {
    var original = document.getElementById('original').value;
    var result = eval(original);
    return result;
}

I'm only doing mathematical operations here, so I was looking for something like eval that would just do math. For example, there's one library called mathjs: https://stackoverflow.com/a/16262947/6110631

I tried using math.eval() (which is now math.evaluate()), as well as the parse function. Some parts of the program work while others seem broken. The same thing happens with other libraries, many of which fail completely. It's hard to trace why because I'll see errors inside the library code, but the HTML output on the page is not the same as with eval, which indicates that the library isn't a suitable drop-in mathematical replacement. The JavaScript Expression Evaluator also has this issue.

One problem is I need to continue supporting the same syntax, which could include:

  • x*x
  • 4-x+2*y
  • 9.8-0.2*x
  • x*x*x*x
  • e

eval handles all of these without issues. Other libraries often have differing syntax or may not support multiple variables, etc. I'm currently using eval because the functions operate directly on user input. Part of the challenge comes from the fact that the variables themselves change throughout execution, and some of these libraries don't seem to have access to their values in the interim.

Is there a library or "safer" eval-alternative available that won't break any current inputs, but allows me to stop using eval itself? In other words, it will return the same results for the same syntax that eval does? Function() didn't work for me, either, but that's a no-go anyways since it's also part of unsafe-eval. This means not just capable of doing all the math, but doing it with the same input syntax, which seems to be tricky (e.g. x*x instead of something like Math.mult()).

Here's a console debug using "Nerdamer":

0
euler.js:53 
Expression {symbol: Symbol}
euler.js:52 0.09
euler.js:53 
Expression {symbol: Symbol}
symbol: Symbol {group: 3, value: "x", multiplier: Frac, power: Frac, imaginary: false, …}
__proto__: Object
euler.js:52 0.36
euler.js:53 
Expression {symbol: Symbol}
symbol: Symbol {group: 3, value: "x", multiplier: Frac, power: Frac, imaginary: false, …}
__proto__: Object

Line 52 is using eval - Line 53 is using the "Nerdamer" library.

52 console.log(eval(original));

53 console.log(nerdamer(original).evaluate());

I'm not interesting in sanitizing the expression before using eval. I'm not worried about processing user input; I'd like to be able to stop using eval so I can remove it from my CSP.

Full Code:

I tried to set this up in a jsFiddle but for some reason could not get it to work, there. Here is the minimal HTML needed:

    <table>
        <tr>
            <td><span>f(x) &mdash; if known: <span></td>
            <td><input id="original" size="30" value="(x*x*x)/3 + 1"></input></td>
        </tr>
        <tr>
            <td><span>f&nbsp;&#39;(x): <span></td>
            <td><input id="deriv" size="30" value="x*x"></input></td>
        </tr>
        <tr>
            <td><span>Starting x-value: <span></td>
            <td><input id="xinitial" size="30" value="0"></input></td>
        </tr>
        <tr>
            <td><span>Starting y-value: <span></td>
            <td><input id="yinitial" size="30" value="1"></input></td>
        </tr>
        <tr>
            <td><span>Final x-value: <span></td>
            <td><input id="xfinal" size="30" value="1.8"></input></td>
        </tr>
        <tr>
            <td><span>Step Size: <span></td>
            <td><input id="stepsize" size="30" value="0.3"></input></td>
        </tr>
    </table>
    <div id="submitbutton">
        <button id="submit">Calculate!</button>
    </div>
    <div>
        <table id="output">
        </table>
        <div id="actual">
        </div>
    </div>

Here is the JS:

var func = 0;
var orig = 0;
var x0 = 0;
var y0 = 0;
var xf = 0;
var stepSize = 0;
var e = 2.718281828459045;
var lastX = 0;
var lastY = 0;
function calculate() {
    document.getElementById('output').innerHTML = "";
    document.getElementById('actual').innerHTML = "";
    orig = document.getElementById('original').value;
    func = document.getElementById('deriv').value;
    stepSize = document.getElementById('stepsize').value;
    x0 = document.getElementById('xinitial').value;
    y0 = document.getElementById('yinitial').value;
    xf = document.getElementById('xfinal').value;
    var numIntervals = (xf-x0)/stepSize;
    slopes = new Array(numIntervals);
    y = new Array(numIntervals);
    document.getElementById('output').innerHTML += '<tr><th>&#916;x</th><th>Slope</th><th>&#916;y</th><th></th><th>x</th><th></th><th>y</th></tr>';
    for (i=0; i<=numIntervals; i++) {
        thisX = parseFloat(x0) + parseFloat(i*stepSize);
        lastX = thisX;
        if (i == 0) {
            y[i] = parseFloat(y0);
            slopes[i] = derivative(thisX, y[i]);
        }
        if (i != 0) {
            y[i] = parseFloat(y[i-1]) + (parseFloat(slopes[i-1]) * parseFloat(stepSize));
            slopes[i] = derivative(thisX, y[i]);
            if (i == numIntervals) {
                lastY = y[i];
            }
        }
        if (i == 0) {
            document.getElementById('output').innerHTML += '<tr>' + '<td>' + parseFloat(stepSize) + '</td><td class="center">' + '&mdash;' + '</td><td class="center">' + '&mdash;' +'</td><td>' + '(</td><td>' + thisX.toFixed(2) + '</td><td>,</td><td>' + y[i].toFixed(5) + '</td><td>)</td>' + '</tr>';
        } else {
            document.getElementById('output').innerHTML += '<tr>' + '<td>' + parseFloat(stepSize) + '</td><td>' + parseFloat(slopes[i-1]).toFixed(3) + '</td><td>' + (parseFloat(slopes[i-1]) * parseFloat(stepSize)).toFixed(3) +'</td><td>' + '(</td><td>' + thisX.toFixed(2) + '</td><td>,</td><td>' + y[i].toFixed(5) + '</td><td>)</td>' + '</tr>';
        }
    }
    if (document.getElementById('original').value != "") {
        correct = origfunc(lastX);
        document.getElementById('actual').innerHTML += '<b>Actual: </b>' + correct.toFixed(3) + '<br>';
        document.getElementById('actual').innerHTML += '<b>Error: </b>' + (correct - lastY).toFixed(3) + '<br>';
    }
}
function derivative(x,y) {
    var fxn = document.getElementById('deriv').value;
    //console.log(nerdamer(fxn).evaluate());
    //console.log(math.evaluate(fxn));
    var result = eval(fxn);
    return result;
}
function origfunc(x) {
    var original = document.getElementById('original').value;
    //console.log(math.evaluate(original));
    var result = eval(original);
    return result;
}
document.getElementById('submit').onclick = function(){calculate()};

The last 3 lines of expected output in this scenario look like:

0.3 2.250   0.675   (   1.80    ,   2.48500 )
Actual: 2.944
Error: 0.459
InterLinked
  • 1,247
  • 2
  • 18
  • 50
  • 2
    If you need a specific syntax to be supported, you may need to convert the input into something the library can read first (Though honestly any mathematics library should understand the basics you've included as examples) Edit: Perhaps you can give us an example of a calculation that fails and it's corresponding result/error? – DBS Aug 13 '20 at 13:13
  • @DBS It's hard to pin it down specifically. For example, I just tried debugging with Nerdamer, and when I try swapping, I get: `Uncaught TypeError: correct.toFixed is not a function`. This is in a different part of the code that isn't changed, but uses the result from origfunction(x), so it looks like a type mismatch, maybe? By the way, when I do math with it, I get NaN, so it can't handle the same input format that eval can. – InterLinked Aug 13 '20 at 13:41
  • 1
    We need a concrete string for which it fails. – trincot Aug 13 '20 at 14:19
  • @trincot I understand, but it seems really that it fails on any input, because of limitations of context of library access to program state. I could publish the full script if that would help. – InterLinked Aug 13 '20 at 14:22
  • A minimal, reproducible piece of code is all that is needed. – trincot Aug 13 '20 at 14:26
  • “This means not just capable of doing all the math, but doing it with the same input syntax, which seems to be tricky (e.g. x^x instead of something like Math.power())” – you mean you want to keep supporting `Math.pow()`? `x^x` doesn’t do exponentiation in `eval`. “Part of the challenge comes from the fact that the variables themselves change throughout execution” doesn’t make sense to me either unless the expression being evaluated should be able to update the values of variables itself. – Ry- Aug 13 '20 at 14:31
  • @Ry- That might have been a bad example. `x*x` works for powers, I can confirm the `x^y` syntax does not work. Updated. – InterLinked Aug 13 '20 at 14:32
  • `x*x` → `x**x`? Also, can you give more details about how you tried math.js and which expressions it failed on? – Ry- Aug 13 '20 at 14:33
  • @Ry- `x**x` does work, is that how powers are done? Learned something new! Anyways, math.js is a complete failure, the script crashes completely and I get: `Undefined symbol x` - this is just doing a console.log() – InterLinked Aug 13 '20 at 14:36
  • It would have been better if you had posted the code that *didn't* work, instead of the working `eval` version. I suspect that you did not pass the "context" to the API you have tried with. All of those you mentioned, provide somewhere an argument where you can specify the set of variables (i.e. x, y) as an object (i.e. {x, y}). – trincot Aug 13 '20 at 14:36
  • 2
    OK, now that you edited your question, it is clear that this ^^ is the reason why it fails. You must provide that extra argument. It explains also the `Undefined symbol x` error. – trincot Aug 13 '20 at 14:38
  • 1
    See the examples with “scope” at https://mathjs.org/examples/expressions.js.html. (No library is able to read your locals, you always have to pass them explicitly.) – Ry- Aug 13 '20 at 14:40
  • @trincot I believe you are right, thank you! That seems to do it. I will do some testing now but I think that was it, so I'll close the question. – InterLinked Aug 13 '20 at 14:40

0 Answers0