add (optional) console.log to SES realms
MODIFY
src/bundle/createSES.js
src/bundle/createSES.js
@@ -22,6 +22,7 @@ import getAnonIntrinsics from './anonIntrinsics.js';
import { deepFreeze } from './deepFreeze.js';
import hardenPrimordials from './hardenPrimordials.js';
import whitelist from './whitelist.js';
+import { makeConsole } from './make-console.js';
export function createSESWithRealmConstructor(creatorStrings, Realm) {
function makeSESRootRealm(options) {
@@ -83,17 +84,26 @@ export function createSESWithRealmConstructor(creatorStrings, Realm) {
shims.push(removeProp);
let r = Realm.makeRootRealm({shims: shims});
+
const b = r.evaluate(creatorStrings);
b.createSESInThisRealm(r.global, creatorStrings, r);
//b.removeProperties(r.global);
r.global.def = b.def;
r.global.Nat = b.Nat;
+ if (options.consoleMode === "allow") {
+ const s = `(${makeConsole})`;
+ r.global.console = r.evaluate(s)(console);
+ }
+
+ // Finally freeze all the primordials, and the global object. This must
+ // be the last thing we do.
const hardenPrimordialsSrc = `
const deepFreeze = (${deepFreeze});
const getAnonIntrinsics = (${getAnonIntrinsics});
(${hardenPrimordials})`;
r.evaluate(hardenPrimordialsSrc)(r.global);
+
return r;
}
const SES = {

ADD
src/bundle/make-console.js
+
+export function makeConsole(parentConsole) {
+ /* 'parentConsole' is the parent Realm's original 'console' object. We must
+ wrap it, exposing a 'console' with a 'console.log' (and perhaps others)
+ to the local realm, without allowing access to the original 'console',
+ its return values, or its exception objects, any of which could be used
+ to break confinement via the unsafe Function constructor. */
+
+ // callAndWrapError is copied from proposal-realms/shim/src/realmFacade.js
+ // Like Realm.apply except that it catches anything thrown and rethrows it
+ // as an Error from this realm
+
+ const errorConstructors = new Map([
+ ['EvalError', EvalError],
+ ['RangeError', RangeError],
+ ['ReferenceError', ReferenceError],
+ ['SyntaxError', SyntaxError],
+ ['TypeError', TypeError],
+ ['URIError', URIError]
+ ]);
+
+ function callAndWrapError(target, ...args) {
+ try {
+ return target(...args);
+ } catch (err) {
+ if (Object(err) !== err) {
+ // err is a primitive value, which is safe to rethrow
+ throw err;
+ }
+ let eName, eMessage, eStack;
+ try {
+ // The child environment might seek to use 'err' to reach the
+ // parent's intrinsics and corrupt them. `${err.name}` will cause
+ // string coercion of 'err.name'. If err.name is an object (probably
+ // a String of the parent Realm), the coercion uses
+ // err.name.toString(), which is under the control of the parent. If
+ // err.name were a primitive (e.g. a number), it would use
+ // Number.toString(err.name), using the child's version of Number
+ // (which the child could modify to capture its argument for later
+ // use), however primitives don't have properties like .prototype so
+ // they aren't useful for an attack.
+ eName = `${err.name}`;
+ eMessage = `${err.message}`;
+ eStack = `${err.stack}`;
+ // eName/eMessage/eStack are now child-realm primitive strings, and
+ // safe to expose
+ } catch (ignored) {
+ // if err.name.toString() throws, keep the (parent realm) Error away
+ // from the child
+ throw new Error('unknown error');
+ }
+ const ErrorConstructor = errorConstructors.get(eName) || Error;
+ try {
+ throw new ErrorConstructor(eMessage);
+ } catch (err2) {
+ err2.stack = eStack; // replace with the captured inner stack
+ throw err2;
+ }
+ }
+ }
+
+ const newConsole = {};
+ const passThrough = ['log', 'info', 'warn', 'error',
+ 'group', 'groupEnd',
+ 'trace',
+ 'time', 'timeLog', 'timeEnd'];
+ // TODO: those are the properties that MDN documents. Node.js has a bunch
+ // of additional ones that I didn't include, which might be appropriate.
+
+ passThrough.forEach(name => {
+ // TODO: do we reveal the presence/absence of these properties to the
+ // child realm, thus exposing nondeterminism (and a hint of what platform
+ // you might be on) when it is constructed with {consoleMode: allow} ? Or
+ // should we expose the same set all the time, but silently ignore calls
+ // to the missing ones, to hide that variation? We might even consider
+ // adding console.* to the child realm all the time, even without
+ // consoleMode:allow, but ignore the calls unless the mode is enabled.
+ if (name in parentConsole) {
+ const orig = parentConsole[name];
+ // TODO: in a stack trace, this appears as
+ // "Object.newConsole.(anonymous function) [as trace]"
+ // can we make that "newConsole.trace" ?
+ newConsole[name] = function(...args) {
+ callAndWrapError(orig, ...args);
+ };
+ }
+ });
+
+ return newConsole;
+}
+

ADD
test/test-console.js
+import test from 'tape';
+import SES from '../src/index.js';
+
+test('console disabled by default', function(t) {
+ const s = SES.makeSESRootRealm({errorStackMode: "allow"});
+ t.equal(typeof s.global.console, "undefined");
+ t.throws(() => s.evaluate('console.log("console is missing entirely")'), TypeError);
+ t.end();
+});
+
+test('console can be enabled', function(t) {
+ const s = SES.makeSESRootRealm({consoleMode: "allow"});
+ console.log("you should see 6 messages between here:");
+ s.evaluate('console.log("1/6 log (internal)")');
+ s.global.console.log("2/6 log (external)");
+ s.evaluate('console.info("3/6 info")');
+ s.evaluate('console.warn("4/6 warn")');
+ s.evaluate('console.error("5/6 error")');
+ s.evaluate('console.time("6/6 console.time")');
+ s.evaluate('console.timeEnd("6/6 console.time")');
+ console.log("and here");
+ t.end();
+});
+
+test('console should be frozen', function(t) {
+ const s = SES.makeSESRootRealm({consoleMode: "allow"});
+ t.throws(() => s.evaluate('console.forbidden = 4'), TypeError);
+ t.throws(() => s.evaluate('console.log = 4'), TypeError);
+ t.throws(() => s.evaluate('console.log.forbidden = 4'), TypeError);
+ t.end();
+});

add (optional) console.log to SES realms