Leaking the Observable runtime in a module

Hi, just thought I would post this here, cause I don’t think that’s a major security problem, but I managed to leak the runtime object in a notebook by importing a broken module that contained an “eval” statement.

See:

and

2 Likes

Oh wow, that’s so much better than the ugly hack in Accessing a Notebook’s Runtime, thanks for sharing!

Note that you don’t have to use two notebooks. Instead just reimport from the current one:

r = {
  try { return eval('runtime') }
  catch(e) {}
}
import {r as runtime} from '@edeboursetty/runtime-hack-part-i' // this notebook

And then e.g.

Object.getOwnPropertyNames(runtime)
Array.from(runtime._variables).map(v => v._name)
1 Like

I would also recommend to generalize this to

getScoped = name => {
  try { return eval(name); }
  catch(e) {}
}

then

import {getScoped as _getScoped} from 'THIS_NOTEBOOK'

and e.g.

_getScoped('define1')

Alright, so I’ve been trying to understand why this only works when called inside a module, and the reason is Observable’s editor UI.

As we know eval runs in the scope in which it is called, unless it has been called indirectly (e.g. window.eval instead of eval). In that case the code only has access to the global scope. Spoiler: The eval that we call in a notebook is a direct reference.

When we download and host the notebook locally, and run eval("runtime") directly, we actually get access to the runtime instance. So what’s different?

Observable dynamically reevaluates each cell’s snippet when you edit a cell’s code. It does so via the following function:

function Cn(e, t) {
    try {
        return (0,
        eval)(`"use strict";(\n${e}\n)\n//# sourceURL=observablehq-${t}`)
    } catch (e) {
        return function(e) {
            return ()=>{
                throw e
            }
        }(e)
    }
}

where e contains the compiled cell source, and t contains the node/cell ID. When we take a closer look we’ll note (0, eval), which is eval being called indirectly.

Modules on the other hand get loaded the good old-fashioned way via import():

                    a = async function(e, t) {
                    if ("string" == typeof e)
                        return import(e).catch((()=>{
                            throw new TypeError(`Notebook '${t}' failed to load`)
                        }
                        ));

which is why there’s no indirection, and thus eval has access to the variable’s scope (and its parent scopes).

1 Like

I don’t see a security problem here. (As @mootari demonstrated, there are other ways to access the current Runtime.) Our primary security model is the sandbox: the sandbox is restricted, but within the sandbox, you can write whatever code you want. It’s not feasible to try to enforce restrictions within the sandbox with a highly dynamic language like JavaScript.

That said, I also would not rely on this technique! This could easily break with changes to our compiler in the future.

Cool, I didn’t know about @mootari’s attempt. Javascript lets you do crazy stuff indeed…

Re: security, yeah the sandbox is definitely the barrier. I was worried that the observer had access somehow to the main page. I have no idea how you guys do to change the _offset from the observer when opening/closing the code editor for instance.

Via messages sent from the parent frame to the sandbox: Observable Editor Events / Fabian Iwand / Observable

Messages from the sandbox are sent for stuff like content height changes, extracting the title and downloading data/images. type window.addEventListener('message', e => console.log(e.data)) in the dev tools console (with the “top” context selected) to monitor them.

1 Like