Mirror: A maybe slightly safer-ish wrapper around eval Function constructors

Compare changes

Choose any two refs to compare.

Changed files
+122 -32
.github
workflows
src
+26
.github/workflows/mirror.yml
···
+
# Mirrors to https://tangled.sh/@kitten.sh (knot.kitten.sh)
+
name: Mirror (Git Backup)
+
on:
+
push:
+
branches:
+
- main
+
jobs:
+
mirror:
+
runs-on: ubuntu-latest
+
steps:
+
- name: Checkout repository
+
uses: actions/checkout@v4
+
with:
+
fetch-depth: 0
+
fetch-tags: true
+
- name: Mirror
+
env:
+
MIRROR_SSH_KEY: ${{ secrets.MIRROR_SSH_KEY }}
+
GIT_SSH_COMMAND: 'ssh -o StrictHostKeyChecking=yes'
+
run: |
+
mkdir -p ~/.ssh
+
echo "$MIRROR_SSH_KEY" > ~/.ssh/id_rsa
+
chmod 600 ~/.ssh/id_rsa
+
ssh-keyscan -H knot.kitten.sh >> ~/.ssh/known_hosts
+
git remote add mirror "git@knot.kitten.sh:kitten.sh/${GITHUB_REPOSITORY#*/}"
+
git push --mirror mirror
+2 -1
package.json
···
"name": "evalish",
"description": "A maybe slightly safer-ish wrapper around eval Function constructors",
"DISCLAIMER": "Please maybe try something else first.. Please.",
-
"version": "0.1.6",
+
"version": "0.1.8",
"main": "dist/evalish.js",
"module": "dist/evalish.mjs",
"types": "dist/types/index.d.ts",
···
"@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-commonjs": "^21.0.2",
"@rollup/plugin-node-resolve": "^13.1.3",
+
"@types/node": "^18.0.6",
"@types/react": "^17.0.42",
"husky-v4": "^4.3.8",
"lint-staged": "^12.3.7",
+88 -30
src/index.ts
···
require: true,
Function: true,
eval: true,
+
process: true,
module: true,
exports: true,
makeSafeGlobal: true,
···
};
const noop = function () {} as any;
+
const _freeze = Object.freeze;
+
const _seal = Object.seal;
+
const _keys = Object.keys;
+
const _getOwnPropertyNames = Object.getOwnPropertyNames;
+
const _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
+
const _defineProperty = Object.defineProperty;
+
const _create = Object.create;
+
const _slice = Array.prototype.slice;
type Object = Record<string | symbol, unknown>;
···
}
function freeze(target: Object): Object {
-
return typeof Object.freeze === 'function'
-
? Object.freeze(target)
-
: target;
+
try { _freeze(target); } catch (_error) {}
+
try { _seal(target); } catch (_error) {}
+
return target;
}
+
const masked = new Set();
+
// Wrap any given target with a masking object preventing access to prototype properties
-
function mask(target: any) {
+
function mask(target: any, toplevel: boolean) {
if (
target == null ||
(typeof target !== 'function' && typeof target !== 'object')
···
// If the target isn't a function or object then skip
return target;
}
+
+
if (!('constructor' in target)) {
+
toplevel = false;
+
}
+
+
if (toplevel && masked.has(target)) {
+
return target;
+
} else if (toplevel) {
+
masked.add(target);
+
}
+
// Create a stand-in object or function
-
const standin =
-
typeof target === 'function'
+
let standin = target;
+
if (!toplevel) {
+
standin = typeof target === 'function'
? (function (this: any) {
-
return target.apply(this, arguments);
+
if (new.target === undefined) {
+
return target.apply(this, arguments);
+
} else {
+
const args = _slice.call(arguments);
+
args.unshift(null);
+
return new (target.bind.apply(target, args));
+
}
})
-
: Object.create(null);
+
: _create(null);
+
}
+
// Copy all known keys over to the stand-in and recursively apply `withProxy`
// Prevent unsafe keys from being accessed
const keys = ["__proto__", "constructor"];
···
// Chromium already restricts access to certain globals in an
// iframe, this try catch block is to avoid
// "Failed to enumerate the properties of 'Storage': access is denied for this document"
-
keys.push(...Object.getOwnPropertyNames(target));
-
} catch (e) {}
+
keys.push(..._getOwnPropertyNames(target));
+
} catch (_error) {
+
keys.push(..._keys(target));
+
}
+
const seen = new Set();
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
-
if (
+
if (seen.has(key)) {
+
continue;
+
} else if (
key !== 'prototype' &&
(typeof standin !== 'function' || (key !== 'arguments' && key !== 'caller'))
) {
-
Object.defineProperty(standin, key, {
-
enumerable: true,
-
get: safeKey(target, key)
-
? () => {
-
return typeof target[key] === 'function' ||
-
typeof target[key] === 'object'
-
? mask(target[key])
-
: target[key];
+
seen.add(key);
+
const descriptor = _getOwnPropertyDescriptor(standin, key) || {};
+
if (descriptor.configurable) {
+
_defineProperty(standin, key, {
+
enumerable: descriptor.enumerable,
+
configurable: descriptor.configurable,
+
get: (() => {
+
if (!safeKey(target, key)) {
+
return noop;
+
} if (toplevel) {
+
try {
+
const value = mask(target[key], false);
+
return () => value;
+
} catch (_error) {
+
return noop;
+
}
+
} else {
+
return () => mask(target[key], false);
}
-
: noop,
-
});
+
})(),
+
});
+
}
}
}
-
if (standin.prototype != null)
-
standin.prototype = freeze(Object.create(null));
-
return freeze(standin);
+
+
if (standin.prototype != null) {
+
standin.prototype = _create(null);
+
}
+
+
return toplevel ? standin : freeze(standin);
}
let safeGlobal: Record<string | symbol, unknown> | void;
···
// Get all available global names on `globalThis` and remove keys that are
// explicitly ignored
-
const trueGlobalKeys = Object.getOwnPropertyNames(trueGlobal).filter(
+
const trueGlobalKeys = _getOwnPropertyNames(trueGlobal).filter(
key => !ignore[key]
);
···
document.head.appendChild(iframe);
// We copy over all known globals (as seen on the original `globalThis`)
// from the new global we receive from the iframe
-
vmGlobals = Object.create(null);
+
vmGlobals = _create(null);
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
const key = trueGlobalKeys[i];
vmGlobals[key] = iframe.contentWindow![key];
···
} finally {
if (iframe) iframe.remove();
}
+
} else if (typeof require === 'function') {
+
vmGlobals = _create(null);
+
const scriptGlobal = new (require('vm').Script)('exports = globalThis').runInNewContext({}).exports;
+
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
+
const key = trueGlobalKeys[i];
+
vmGlobals[key] = scriptGlobal[key];
+
}
}
-
safeGlobal = Object.create(null);
+
safeGlobal = _create(null);
// The safe global is initialised by copying all values from either `globalThis`
// or the isolated global. They're wrapped using `withProxy` which further disallows
// certain key accesses
for (let i = 0, l = trueGlobalKeys.length; i < l; i++) {
const key = trueGlobalKeys[i];
-
safeGlobal[key] = mask(vmGlobals[key]);
+
safeGlobal[key] = mask(vmGlobals[key], true);
}
// We then reset all globals that are present on `globalThis` directly
···
for (const key in ignore) safeGlobal[key] = undefined;
// It _might_ be safe to expose the Function constructor like this... who knows
safeGlobal!.Function = SafeFunction;
+
// Lastly, we also disallow certain property accesses on the safe global
// Wrap any given target with a Proxy preventing access to unscopables
if (typeof Proxy === 'function') {
···
// Return a value, if it's allowed to be returned and mask this value
get(target, _key) {
const key = safeKey(target, _key);
-
return key !== undefined ? target[key] : undefined;
+
return !ignore[_key] && key !== undefined ? target[key] : undefined;
},
has(_target, _key) {
return true;
···
}));
} else {
// NOTE: Some property accesses may leak through here without the Proxy
-
return (safeGlobal = mask(safeGlobal));
+
return freeze(safeGlobal!);
}
}
+1 -1
tsconfig.json
···
{
"compilerOptions": {
-
"types": [],
+
"types": ["node"],
"baseUrl": "./",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
+5
yarn.lock
···
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.4.tgz#48aedbf35efb3af1248e4cd4d792c730290cd5d6"
integrity sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==
+
"@types/node@^18.0.6":
+
version "18.0.6"
+
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.6.tgz#0ba49ac517ad69abe7a1508bc9b3a5483df9d5d7"
+
integrity sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==
+
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"