tc39 / proposal-ses

Draft proposal for SES (Secure EcmaScript)
223 stars 20 forks source link

question for use case #44

Closed chris-kruining closed 3 years ago

chris-kruining commented 3 years ago

Hi,

To give some context; I currently have a custom render/template engine for web components, in these templates I allow JS executions via the classic moustache templates. What I currently do is iterate over my HTML to detect these templates and extract the code into a -hopefully- mostly secure sandbox (as far as that is even possible) via the proxy + with trick. however I would like to do this initial pass of "code extraction" on the server to lessen the load on my clients. BUT, the with keyword is not allowed in use-strict code, which I am bound to because I use es6-modules, classes, etc, etc. so therefor I have lost my method of sandboxing when I wish to implement my server side "rendering".

According to the good folks from the Realms proposal would this proposal allow me to make isolated scopes inside which is only available what I inject into it? Ergo sandboxing in the strictest sense of the word.

Here's an example of what my code currently does (Which I know is bad, I want to be able to remove unsafe-eval from my CSP, which is why I'm looking into proper sandboxing).

this is my "raw" HTML

<section>
    <header>        
        <b>{{ t('data.diff.current') }}</b>

        <b>{{ t('data.diff.new') }}</b>
    </header>

    <main :for="{{ product of products }}">
        <fyn-data-product product="{{ product }}"></fyn-data-product>
    </main>
</section>

this is the same HTML after the templating went over it

<section>
    <header>
        <b>{#c4acc914-0348-4a17-897a-18b0f0d8dd90}</b>

        <b>{#b43300d3-9366-40b5-ade2-afce0479ac64}</b>
    </header>

    <main :for="{#6474986d-443e-4501-90f3-8766b898e974}"></main>
</section>

this is the map the templating made for this fragment

{
    "a199308d-7564-498a-a2e9-7307ed3f5fde": {
        callable: (async function anonymous(t) {
            const sandbox = new Proxy({ Math, JSON, Date, range, t }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await t('data.diff.current');
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [],
        code: "t('data.diff.current')",
        original: "{{ t('data.diff.current') }}",
    },
    "f855355b-c0eb-4570-a314-d33ee4550f7d": {
        callable: (async function anonymous(t) {
            const sandbox = new Proxy({ Math, JSON, Date, range, t }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await t('data.diff.new');
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [],
        code: "t('data.diff.new')",
        original: "{{ t('data.diff.new') }}",
    },
    "44e25a1f-2dcf-4109-8987-85a09ce72d34": {
        callable: (async function anonymous(products) {
            const sandbox = new Proxy({ Math, JSON, Date, range, products }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await products;
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [ "products" ],
        code: "product of products",
        original: "{{ product of products }}",
        directive: {
            fragment: Fragment,
            key: "product",
            name: "product",
            type: "for",
        },
    },
};
zarutian commented 3 years ago

What I currently do is iterate over my HTML to detect these templates and extract the code into a -hopefully- mostly secure sandbox (as far as that is even possible) via the proxy + with trick. however I would like to do this initial pass of "code extraction" on the server to lessen the load on my clients. I take it as given that you are 'extracting' code of unknown provience and want to execute it in a safer environment.

According to the good folks from the Realms proposal would this proposal allow me to make isolated scopes inside which is only available what I inject into it? Ergo sandboxing in the strictest sense of the word.

That is the entire point of the Realms proposal afaict. SES is actually more strict than that as the primordials (all objects in the environment prior to code execution other than host objects that give access to the world outside the Realm) are all transitively frozen and immutable.

Here's an example of what my code currently does (Which I know is bad, I want to be able to remove unsafe-eval from my CSP, which is why I'm looking into proper sandboxing).

this is my "raw" HTML

<section>
    <header>        
        <b>{{ t('data.diff.current') }}</b>

        <b>{{ t('data.diff.new') }}</b>
    </header>

    <main :for="{{ product of products }}">
        <fyn-data-product product="{{ product }}"></fyn-data-product>
    </main>
</section>

this is the same HTML after the templating went over it

<section>
    <header>
        <b>{#c4acc914-0348-4a17-897a-18b0f0d8dd90}</b>

        <b>{#b43300d3-9366-40b5-ade2-afce0479ac64}</b>
    </header>

    <main :for="{#6474986d-443e-4501-90f3-8766b898e974}"></main>
</section>

this is the map the templating made for this fragment

{
    "a199308d-7564-498a-a2e9-7307ed3f5fde": {
        callable: (async function anonymous(t) {
            const sandbox = new Proxy({ Math, JSON, Date, range, t }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await t('data.diff.current');
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [],
        code: "t('data.diff.current')",
        original: "{{ t('data.diff.current') }}",
    },
    "f855355b-c0eb-4570-a314-d33ee4550f7d": {
        callable: (async function anonymous(t) {
            const sandbox = new Proxy({ Math, JSON, Date, range, t }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await t('data.diff.new');
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [],
        code: "t('data.diff.new')",
        original: "{{ t('data.diff.new') }}",
    },
    "44e25a1f-2dcf-4109-8987-85a09ce72d34": {
        callable: (async function anonymous(products) {
            const sandbox = new Proxy({ Math, JSON, Date, range, products }, {
                has: () => true,
                get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
            });

            try
            {
                with(sandbox)
                {
                    return await products;
                }
            }
            catch
            {
                return undefined;
            }
        }),
        keys: [ "products" ],
        code: "product of products",
        original: "{{ product of products }}",
        directive: {
            fragment: Fragment,
            key: "product",
            name: "product",
            type: "for",
        },
    },
};

The SES shim uses the same with trick only the sandbox parameter is a Proxy with rather interesting handler.

chris-kruining commented 3 years ago

hmmm, the Realms folks told me that the Realms proposal does not cover security. (https://github.com/tc39/proposal-realms/issues/290).

but since with is out of reach within a strict context, is there an alternative method/trick you know of in which I can create a sandbox? because I'd REALLY love to move the logic I described above to the server side :D

P.S. for context: this is my template / render webcomponent lib, https://github.com/FYN-Software/component. template.js is where the "magic" happens