[RFC] Jest-internal sandbox escape hatch · Issue #9898 · jestjs/jest · GitHub
Skip to content

[RFC] Jest-internal sandbox escape hatch #9898

Closed
@jeysal

Description

@jeysal

Motivation

This stems from #7792, where we need to load multiple dependencies outside of the sandbox from jest-snapshot running within the sandbox. Passing things in from the outside is becoming very tedious, and even passing in a requireOutside function doesn't seem easy enough - it should be easy for our own code to make the right call on when to import from outside the sandbox for correctness (not being influenced by mocking/module mutation from user code) and perf (not creating many new module instances).

jest-snapshot will now need @babel/{core,traverse,types,generator} and prettier from outside the sandbox. I can also imagine that in future extensions of inline snapshot logic, and other parts of Jest loaded inside the sandbox, we may want to require outside dependencies, both for performance and for correctness (not requiring mocks or modules modified by the user). @SimenB has also mentioned that it would be nice if e.g. inside expect could get an outside jest-diff to improve performance.
I also want to avoid this requirement introducing lots of boilerplate into our packages because we have to pass either imported dependency objects or requireOutside functions around, so I'd like to have a way for Jest's own packages to require outside modules more easily, while still preventing user code from doing so (i.e. no globalThis.__requireOutside()). It should be easy for us to correctly require external dependencies, but as close to impossible as we can get for the user.

🚀 Feature Proposal

Here's my proposal, slightly wild but I think the downsides can actually be mitigated:
jest-runtime adds a require(moduleName, {outside: true}) to the createRequireImplementation() return value, but only for modules required as internal (fortunately we already know this info) to make sure user code doesn't get it.
We already have code around this, the require implementation for internal modules is already different in that it, firstly, requires further modules as internal as well, and secondly, never considers mocks.
This require interface extension is compatible with the official Node require, so loading Jest packages using outside: true when already outside of a sandbox will still work. This applies both for us, if we load util code sometimes inside and sometimes outside a sandbox, and for external users of jest-diff etc.
Because the outside option will (CAN) only be used by Jest's internal modules that also makes it okay in the unlikely event that Node ever decides to add a second parameter to require - we can refactor our interface and all usages to make it compatible again.

Variants

Node API compatibility

@SimenB voiced concerns regarding Node API compatibility if we extend require.
A possible way to fix this:
We have a Babel plugin (applied to Jest's internal modules and ideally just a macro) transform requireOutside('/module.js') to require(require.resolve('/module.js', {outsideJestVm: true})). The runtime's resolve returns 'jest-outside-vm:///module.js'. This protocol is handled by the runtime's require, prompting it to use the real require to retrieve the module. This should be a 100% compliant require implementation, unless you consider coming up with extra options forbidden.
This does expose a way for users to break outside of the sandbox again, if they know the protocol. To secure against this, we can generate a random crypto number on runtime startup and attach that to the protocol, or even an HMAC over the path using the number, so that in case we leak one such URL it cannot be used to require any file from outside the VM.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions