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:
http and
crypto.atob, TextEncoder, and URL.import expressionsHardenedJS adds the following to JavaScript or changes them significantly:
lockdown()harden()Compartmentconsoleassertlockdown(options)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(options)repairIntrinsics() tames some objects, such as:
String.prototype.localeCompare() with lexical
versions that do not reveal the user locale.hardenIntrinsics()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.lockdown() and harden()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.
lockdown Options'safe' settingsAll three of these safety-relevant options default to 'safe' if omitted
from a call to lockdown(). Their other possible value is 'unsafe'.
regExpTaminglocaleTamingconsoleTamingIn addition, errorTaming defaults to 'safe' but can be set to 'unsafe'
or 'unsafe-debug', as explained at
errorTaming Options.
errorTamingThe 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.
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 |
regExpTaming OptionWith 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()
localeTaming OptionThe 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().
toLocaleStringtoLocaleDateStringtoLocaleTimeStringtoLocaleLowerCasetoLocaleUpperCaselocaleCompareThe '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
consoleTaming OptionsThe 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
errorTaming OptionsThe 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 OptionsstackFiltering 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.
overrideTaming OptionsThe 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:
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