EndoJS
    Preparing search index...

    Endo and HardenedJS (SES) Programming Reference

    This document describes how ses creates a HardenedJS mode for safe JavaScript. It is very much a "how to do something" document, with little explanation about why and how something was implemented or other background information. For that, see the more comprehensive Endo and HardenedJS Programming Guide.

    The SES shim transforms ordinary JavaScript environments into HardenedJS environments.

    On Node.js you can import or require ses in either CommonJS or ECMAScript modules, then call lockdown(). This is a shim. It mutates the environment in place so any code running after the shim can assume it’s running in a HardenedJS environment. This includes the globals lockdown(), harden(), Compartment, and so on. For example:

    require("ses");
    lockdown();

    Or:

    import 'ses';
    lockdown();

    To ensure a module runs in a HardenedJS environment, wrap the above code in a ses-lockdown.js module and import it:

    import './non-ses-code-before-lockdown.js';
    import './ses-lockdown.js'; // calls lockdown.
    import './ses-code-after-lockdown.js';

    The Endo project includes packages that do just this:

    • @endo/lockdown calls lockdown and threads certain environment options.
    • @endo/init also sets up eventual send and a more completed Endo environment.

    To use SES as a script on the web, use the UMD build.

    <script src="node_modules/ses/dist/ses.umd.min.js">
    

    To run shims after ses repairs the intrinsics but before ses hardens the intrinsics, calling lockdown(options) is equivalent to running repairIntrinsics(options) then hardenIntrinsics() and vetted shims can run in between.

    import './non-ses-code-before-lockdown.js';
    import './ses-repair-intrinsics.js'; // calls repairIntrinsics.
    import './vetted-shim.js';
    import './ses-harden-intrinsics.js'; // calls hardenIntrinsics.
    import './ses-code-after-lockdown.js';

    SES is vulnerable to any code that runs before hardening intrinsics. All such code, including vetted shims, must receive careful review to ensure it preserves the invariants of the OCap security model.

    The following are missing or unusable under HardenedJS:

    HardenedJS adds the following to JavaScript or changes them significantly:

    • lockdown()
    • harden()
    • Compartment
    • console
    • assert
    • Shared JavaScript primordials are frozen.

    Lockdown performs two operations and these can be separated by calling repairIntrinsics(options) and hardenIntrinsics(). They collectively prepare a realm for safe execution of code in compartments.

    These methods do not erase any powerful objects from the initial global scope. Instead, Compartments give complete control over what powerful objects exist for client code.

    repairIntrinsics() tames some objects, such as:

    • Regular expressions
      • A tamed RexExp does not have the deprecated compile method.
    • Locale methods
      • Lockdown replaces locale methods like String.prototype.localeCompare() with lexical versions that do not reveal the user locale.
    • Errors
      • A tamed error does not have a V8 stack, but the console can still see the stack.

    hardenIntrinsics() tamper-proofs all of the JavaScript intrinsics, so no program can subvert their methods (preventing some man in the middle attacks). Also, no program can use them to pass notes to parties that haven't been expressly introduced (preventing some covert communication channels).

    hardenIntrinsics() freezes all JavaScript defined objects accessible to any program in the realm. The frozen accessible objects include but are not limited to:

    • globalThis
    • [].__proto__ the array prototype, equivalent to Array.prototype in a pristine JavaScript environment.
    • {}.__proto__ the Object.prototype
    • (() => {}).__proto__ the Function.prototype
    • (async () => {}).__proto__ the prototype of all asynchronous functions, and has no alias in the global scope of a pristine JavaScript environment.
    • The properties of any accessible object

    lockdown() and harden() do the same thing; freeze objects so their properties cannot be changed. You can only interact with frozen objects through their methods. Their differences are what objects you use them on, and when you use them.

    lockdown() must be called first. It hardens JavaScript's built-in primordials (implicitly shared global objects) and enables harden(). Calling harden() before lockdown() executes throws an error.

    lockdown() works on objects created by the JavaScript language itself as part of its definition. Use harden() to freeze objects created by your JavaScript code after lockdown()was called.

    All three of these safety-relevant options default to 'safe' if omitted from a call to lockdown(). Their other possible value is 'unsafe'.

    • regExpTaming
    • localeTaming
    • consoleTaming

    In addition, errorTaming defaults to 'safe' but can be set to 'unsafe' or 'unsafe-debug', as explained at errorTaming Options.

    • errorTaming

    The tradeoff is safety vs compatibility with existing code. However, much legacy JavaScript code does run under HardenedJS, even if both not written to do so and with all the options set to 'safe'. Only consider an 'unsafe' value if you both need it and can evaluate its risks.

    This section provides a quick usage reference for lockdown()'s options, their possible values, and their usage. Each is described in more detail in their individual sections below.

    Option
    Values
    Functionality
    regExpTaming 'safe' (default) or 'unsafe' 'safe' disables all RegExp.* methods,
    'unsafe' disables all but RegExp.prototype.compile()
    localeTaming 'safe' (default) or 'unsafe' 'safe' aliases toLocaleString() to toString(), etc.,
    'unsafe' keeps JavaScript locale methods as is
    consoleTaming 'safe' (default) or 'unsafe' 'safe' wraps start console to show deep stacks,
    'unsafe' uses the original start console.
    errorTaming 'safe' (default) or 'unsafe' or 'unsafe-debug' 'safe' denies unprivileged stacks access,
    'unsafe' makes stacks also available by errorInstance.stack,
    'unsafe-debug' sacrifices more safety for better TypeScript line-numbers.
    stackFiltering 'concise' (default) or 'omit-frames' or 'shorten-paths' or 'verbose' 'concise' preserves important deep stack info, omitting likely unimportant frames and shortening paths
    'omit-frames' omits likely unimportant frames
    'shorten-paths' shortens paths to text likely clickable in an IDE
    'verbose' console shows full deep stacks
    overrideTaming 'moderate' (default) or 'min' or 'severe' 'moderate' moderates mitigations for legacy compatibility,
    'min' minimal mitigations for purely modern code,
    'severe' when moderate mitigations are inadequate

    With its default 'safe' value, regExpTaming prevents using RegExp.*() methods in the locked down code.

    With its 'unsafe' value, RegExp.prototype.compile() can be used in locked down code. All other RegExp.*() methods are disabled.

    lockdown(); // regExpTaming defaults to 'safe'
    // or
    lockdown({ regExpTaming: 'safe' }); // Disables all RegExp.*() methods.
    // vs
    lockdown({ regExpTaming: 'unsafe' }); // Disables all RegExp.*() methods except RegExp.prototype.compile()

    The default 'safe' setting replaces each method listed below with their corresponding non-locale-specific method. For example, Object.prototype.toLocaleString() becomes another name for Object.prototype.toString().

    • toLocaleString
    • toLocaleDateString
    • toLocaleTimeString
    • toLocaleLowerCase
    • toLocaleUpperCase
    • localeCompare

    The 'unsafe' setting keeps the original behavior for compatibility at the price of reproducibility and fingerprinting.

    lockdown(); // localeTaming defaults to 'safe'
    // or
    lockdown({ localeTaming: 'safe' }); // Alias toLocaleString to toString, etc
    // vs
    lockdown({ localeTaming: 'unsafe' }); // Allow locale-specific behavior

    The default 'safe' option actually expands what you would expect from console's logging output. It will show information from the assert package and error objects. Errors can report more diagnostic information that should be hidden from other objects. See errors for an in depth explanation of this.

    The 'unsafe' setting leaves the original console in place. The assert package and error objects continue to work, but the console logging output will not show this extra information. 'unsafe' does not remove any additional console methods beyond its de facto "standards". Since we do not know if these methods violate OCap security, we should assume they are unsafe. A raw console object should only be handled by very trustworthy code.

    lockdown(); // consoleTaming defaults to 'safe'
    // or
    lockdown({ consoleTaming: 'safe' }); // Wrap start console to show deep stacks
    // vs
    lockdown({ consoleTaming: 'unsafe' }); // Leave original start console in place

    The errorTaming default 'safe' setting makes the stack trace inaccessible from error instances alone. It does this on v8 engines (Chrome, Brave, Node). Note that it is not hidden on other engines, leaving an information leak available. It reveals information only as a powerless string.

    In JavaScript the stack is only available via err.stack, so some development tools assume it is there. When the information leak is tolerable, the 'unsafe' setting preserves err.stack's filtered stack information.

    The 'safe' or 'unsafe' settings of errorTaming do not affect the Error constructor's safety, beyond the confidentiality hazards mentioned above. After calling lockdown, the tamed Error constructor in the start compartment follows OCap rules. Under v8 it emulates most of the magic powers of the v8 Error constructor—those consistent with the discourse level of the proposed getStack. In all cases, the Error constructor shared by all other compartments is both safe and powerless.

    However, with the 'safe' and 'unsafe' settings, you'll often see line-numbers into TypeScript sources are always 1, since the TypeScript compiler compiles into a single line of JavaScript. For TypeScript on Node on v8, the setting 'unsafe-debug' sacrifices more security to restore the normal Node behavior of providing accurate positions into the TypeScript source. The 'unsafe-debug' setting should be used for development only, when this is usually the right tradeoff. Please do not use it in production.

    lockdown(); // errorTaming defaults to 'safe'
    // or
    lockdown({ errorTaming: 'safe' }); // Deny unprivileged access to stacks, if possible
    // vs
    lockdown({ errorTaming: 'unsafe' }); // Stacks also available by errorInstance.stack
    // vs
    lockdown({ errorTaming: 'unsafe-debug' }); // sacrifice more safety for source-mapped line numbers.

    stackFiltering trades off stronger stack traceback filtering to minimize distractions vs completeness for tracking down bugs in obscure places.

    The default 'concise' setting removes "noise" from the full distributed stack traces, in particularly artifacts from low level infrastructure. It only works on v8 engines.

    With the 'verbose' setting, the console displays the full raw stack information for each level of the "deep stack", tracing back through the eventually sent messages from other turns of the event loop. This makes JavaScript's already voluminous error stacks even more so. However, this is sometimes useful for finding bugs in low level infrastructure.

    Both settings are safe. Stack information will or will not be available from error objects according to the errorTaming option and the platform error behavior.

    lockdown(); // stackFiltering defaults to 'concise'
    // or
    lockdown({ stackFiltering: 'concise' }); // Preserve important deep stack info, omitting likely unimportant frames and shortening paths
    // vs
    lockdown({ stackFiltering: 'omit-frames' }); // Omit likely uninteresting frames
    // vs
    lockdown({ stackFiltering: 'shorten-paths' }); // Shorten paths to text likely clickable in an IDE
    // vs
    lockdown({ stackFiltering: 'verbose' }); // Console shows full deep stacks

    See stackFiltering Options for more.

    The overrideTaming option trades off better code compatibility vs better tool compatibility.

    When starting a project, we recommend using the non-default 'min' option to make debugging more pleasant. You may need to reset it to the 'moderate' default if third-party shimming code interferes with lockdown().

    'moderate' option is intended to be fairly minimal. Expand it when you encounter code which should run under HardenedJS but can't due to the override mistake,

    The 'min' setting serves two purposes:

    • It enables a pleasant VSCode debugging experience.
    • It helps ensure new code does not depend on anything more than enabled legacy code.

    All Agoric-authored code is compatible with both settings, but Agoric currently still pulls in some third party dependencies only compatible with the 'moderate' setting.

    lockdown(); // overrideTaming defaults to 'moderate'
    // or
    lockdown({ overrideTaming: 'moderate' }); // Moderate mitigations for legacy compat
    // vs
    lockdown({ overrideTaming: 'min' }); // Minimal mitigations for purely modern code