microsoft / ClearScript

A library for adding scripting to .NET applications. Supports V8 (Windows, Linux, macOS) and JScript/VBScript (Windows).
https://microsoft.github.io/ClearScript/
MIT License
1.79k stars 148 forks source link

Option to disable the code execution from string (eval and function) #605

Open simontom opened 1 month ago

simontom commented 1 month ago

Hey folks, our scenario is "customer scriptable multitenant" runtime. We want to be very cautious what to allow.

In order to make it a bit more safer, we need the ClearScript API to disable eval and other ECMAScript APIs that convert strings into code (e.g., the Function CTOR).

I see there is some flag in V8 (pro'ly) resulting right in the behaviour we crave for: "--disallow-code-generation-from-strings"

We were able "disable" eval exectuting the following script:

eval = function() {
    throw new Error("The 'eval' function is disabled.");
}

This is what I've tried with function CTOR so far

test('use function for code generation', async (t) => {
    // Has no effect
    (function () { }).constructor = null;
    // so ....
    // These are the main problem
    const FuncCtor = (function () { }).constructor;
    const AsyncFuncCtor = (async function () { }).constructor;

    const log1 = new FuncCtor('str', 'console.log(str);');
    log1('Hello, world! 1');

    const fetchURL = new AsyncFuncCtor('url', 'return await fetch(url);');
    await fetchURL("https://www.google.com")
        .then((res) => res.text())
        .then((text) => text.slice(0, 100))
        .then(console.log);

    // This is fine, throws (but the lines above....)
    Function = null;
    const log2 = new Function('str', 'console.log(str);');
    log2('Hello, world! 2');
});
ClearScriptLib commented 1 month ago

Hi @simontom,

How about something like this:

engine.Execute(@"(() => {
    const AsyncFunction = (async () => {}).constructor;
    const ctor = (function () { throw new Error('Function constructors are disabled'); }).bind();
    Object.defineProperty(Function.prototype, 'constructor', { value: ctor });
    Object.defineProperty(AsyncFunction.prototype, 'constructor', { value: ctor });
    Function = new Proxy(Function, { construct: ctor, apply: ctor });
})()");

Please let us know if that works for you. Thanks!

simontom commented 1 month ago

Alright, folks, it seems it's working like a charm :magic_wand: :green_heart:

test('test 1', async (t) => {
    // Disable Function constructor (works like a charm)
    const AsyncFunction = (async () => {}).constructor;
    const newThrowingCtor = (function () {
        throw new Error('Function constructors are disabled');
    }).bind();
    Object.defineProperty(Function.prototype, 'constructor', {value: newThrowingCtor});
    Object.defineProperty(AsyncFunction.prototype, 'constructor', {value: newThrowingCtor});
    Function = new Proxy(Function, {construct: newThrowingCtor, apply: newThrowingCtor});

    try {
        const FuncCtor = (function () {}).constructor;

        const log = new FuncCtor('str', 'console.log(str);');
        log('Hello, world! 1');
    } catch (e) {
        console.log(e);
    }

    try {
        let AsyncFuncCtor = (async function () {}).constructor;

        const fetchURL = new AsyncFuncCtor('url', 'return await fetch(url);');
        await fetchURL("https://www.google.com")
            .then((res) => res.text())
            .then((text) => text.slice(0, 100))
            .then(console.log);
    } catch (e) {
        console.log(e);
    }

    try {
        const log = new Function('str', 'console.log(str);');
        log('Hello, world! 2');
    } catch (e) {
        console.log(e);
    }
});
Error: Function constructors are disabled
    at new <anonymous> (/TypeScriptWorkbench/test/eval-and-function.test.mjs:31:15)
    at TestContext.<anonymous> (/TypeScriptWorkbench/test/eval-and-function.test.mjs:40:21)
    at Test.runInAsyncScope (node:async_hooks:206:9)
    at Test.run (node:internal/test_runner/test:631:25)
    at Test.processPendingSubtests (node:internal/test_runner/test:374:18)
    at Test.postRun (node:internal/test_runner/test:715:19)
    at Test.run (node:internal/test_runner/test:673:12)
    at async startSubtest (node:internal/test_runner/harness:216:3)
Error: Function constructors are disabled
    at new <anonymous> (/TypeScriptWorkbench/test/eval-and-function.test.mjs:31:15)
    at TestContext.<anonymous> (/TypeScriptWorkbench/test/eval-and-function.test.mjs:49:26)
    at Test.runInAsyncScope (node:async_hooks:206:9)
    at Test.run (node:internal/test_runner/test:631:25)
    at Test.processPendingSubtests (node:internal/test_runner/test:374:18)
    at Test.postRun (node:internal/test_runner/test:715:19)
    at Test.run (node:internal/test_runner/test:673:12)
    at async startSubtest (node:internal/test_runner/harness:216:3)
Error: Function constructors are disabled
    at file:///C:/_git/slack/prodaas/TypeScriptWorkbench/test/eval-and-function.test.mjs:31:15
    at TestContext.<anonymous> (/TypeScriptWorkbench/test/eval-and-function.test.mjs:59:21)
    at Test.runInAsyncScope (node:async_hooks:206:9)
    at Test.run (node:internal/test_runner/test:631:25)
    at Test.processPendingSubtests (node:internal/test_runner/test:374:18)
    at Test.postRun (node:internal/test_runner/test:715:19)
    at Test.run (node:internal/test_runner/test:673:12)
    at async startSubtest (node:internal/test_runner/harness:216:3)

IMHO, an option / flag might be a better solution for future handling of such security :bulb:

simontom commented 1 month ago

Thanks a bunch for your lightning-fast help 🙇