The primary focus of jShelter is to provide security and privacy oriented wrappers of JavaScript APIs. As some of the code is very similar for each wrapper, we use a unified approach to describe wrappers. The approach is described below. This approach also helps to automatically modify toString
conversions of the wrapped APIs, i.e. a correctly written wrapper creates a modified function but wrapper.toString()
returns the original string.
File structure
wrapping.js
provides main facilities for interacting with specific wrappers:add_wrappers()
has to be provided with all the wrappers. Hence, a typical module with wrappers of a web standard APIS. ends with a call ofadd_wrappers(list_of_all_wrappers_defined_by_the_module)
.build_wrapping_code
contains all the registered wrappers. The keys are referenced by the levels wrapper list. Do not modify thebuild_wrapping_code
variable directly, useadd_wrappers
instead.- The module also provides common functions used by the wrappers, e.g.
rounding_function
andnoise_function
. wrappingS-XYZ
are files dealing with APIs introduced by specific web or ECMA standard. See the naming conventions for details on the XYZ part of the name. See the wrapper specification section for the properties of the wrapper objects.
Naming conventions
jShelter adopted the naming conventions of the Web API Manager. See the paper:
Peter Snyder, Cynthia Taylor, and Chris Kanich, “Most Websites Don’t Need to Vibrate: A Cost–Benefit Approach to Improving Browser Security,” in Proceedings of the 2017 ACM Conference on Computer and Communications Security, 2017.
Wrapper specification
Once you are set with the file name for your wrappers, you can start coding. The basic structure of the file is:
(function() {
// definition of common functions used by the wrappers bellow
var wrappers = [
{
// wrapping object 1
},
{
// wrapping object 2
},
...
{
// wrapping object n
},
]
add_wrappers(wrappers);
})();
The content of the module should be in an IIFE so that it does not polute the global namespace. The property values are strings that are generally interpreted either as identifiers or JS code.
Each wrapping object must have the following mandatory properties:
parent_object
andparent_object_property
are used to define the name of the wrapping (parent_object.parent_object_property
) that is referenced by level wrappers. Additionally, it is used ifwrapper_prototype
is defined to provide the object name to have the prototype changed. Finally,Object.freeze
can be optionally called onparent_object.parent_object_property
.apply_if
optionally provides a condition that needs to be fullfilled to apply the wrapper. For example if a wrapper should be applied only when an API already provides some information. For example,apply_if: "navigator.plugins.length > 0"
.wrapped_objects
is a list of objects, each having the following properties (1 mandatory, 2 optional, typically, wrappers use one of the optional names in the wrapper code to access the original result of the call):original_name
(mandatory) - the original name of the object to be wrapped. Do not mentionwindow
here!wrapped_name
- the variable name that points to the actual original object. Wrappers might need it for identity comparisons or other instance-specific use cases. The variable name can be used bywrapping_function_body
,helping_code
and other code fragments to reference the original object. Note that this name is not available outside the code generated by this wrappper.callable_name
- is similar to thewrapped_name
but the variable refers to a proxy of the original object. The proxy intercepts calls and automatically marshals parameters and return values. Wrappers MUST definecallable_name
if the object is passed to other code likePromise
objects or callbacks. The variable is available only in the code generate by this wrapper.callable_name
is specifically meant to be used for native methods and functions which the wrapper needs to call. This is especially important if it accepts callback arguments or returnsPromise
objects: invoking them through theircallable_name
automates complex steps otherwise required for sandboxed browser extensions to interact with web pages.
Generally speaking, use wrapped_name
whenever you need to access the original objects only inside
the wrapper and you do not need to pass the object to other code, such as Promise
objects or
callbacks. Compared to callable_name
, wrapped_name
has less overhead and is the preferred way.
Each wrapping object can have the following optional properties:
wrapping_function_args
is a string that is used as an argument list for the wrapping function. Typically this string reflects the parameters of the original method, it can be an empty string, a list of parameters, such as"source, target, color"
or"...args"
.wrapping_function_body
specified the behaviour of the wrapped function. You can provide a completely different implementation but often, you want to refer to the original implementation (available in the variablewrapped_name
) and modify the original implementation.wrapping_code_function_name
can be used to call the wrapping function from the other code inside the wrapper. For example to create another wrapper similar to the original wrapper.wrapper_prototype
- if defined,parent_object.parent_object_property
prototype is set to the prototype identifier provided bywrapper_prototype
.original_function
if not provided, it defaults toparent_object.parent_object_property
. This name is used for overloading thetoString
function. Instead of the wrapping code,toString
returns the content of the original function.helping_code
provides you an option to define code available for bothreplacement
function andpost_wrapping_code
replace_original_function
is used to control which function should be replacedpost_replacement_code
Allows to provide additional code with the access to the original function and to the wrapped functionfreeze
if set totrue
causes the API to be freezed. It should not be necessary if the wrapping is performed at the right level of the object's prototype chain (i.e. where the property to be modified was originally defined, i.e. usually in the object's prototype). Use this option with caution, only if strictly needed, because native APIs are configurable (otherwise our wrapping couldn't work) and therefore freezing them introduces a "weirdness", exploitable for fingerprinting.post_wrapping_code
is a list of additional wrapping objects with a similar structure to the wrappers, see the section bellow.
Post wrapping code
Complex wrappers need to provide additional wrapping code.
You can make the generation of the post wrapping code generation conditional by using apply_if
, e.g.:
{
...
apply_if: "!enableGeolocation",
}
(enableGeolocation
is a Booloean variable)
Currently jShelter supports additional wrapping of:
- Definition of a function (used, for example, to reintroduce
Date.now()
function to the wrappedDate
object)
[
{
code_type: "function_define",
original_function: "originalDateConstructor.now",
parent_object: "window.Date",
parent_object_property: "now",
wrapping_function_args: "",
wrapping_function_body: "return func(originalDateConstructor.now.call(Date), precision);",
},
]
- Export a function from the wrapping namespace (currently not used, the following code exposes the unwrapped, i.e. original, version of Date.now to page scripts)
[
{
code_type: "function_export",
parent_object: "window.Date",
parent_object_property: "now",
export_function_name: "originalDateConstructor.now",
},
]
- Redefine an object property (used to prevent leaking iframe properties to the unwrapped objects):
{
code_type: "object_properties",
parent_object: "HTMLIFrameElement.prototype",
parent_object_property: "contentWindow",
wrapped_objects: [
{
original_name: "HTMLIFrameElement.prototype.__lookupGetter__('contentWindow')",
wrapped_name: "cw",
},
],
wrapped_properties: [
{
property_name: "get",
property_value: `
function() {
var parent=cw.call(this);
try {
parent.HTMLCanvasElement;
}
catch(d) {
return; // HTMLIFrameElement.contentWindow properties could not be accessed anyway
}
wrapping(parent);
return parent;
}`,
},
],
}
- Deleting a property (used when you want to completely disable an API):
{
code_type: "delete_properties",
parent_object: "navigator",
apply_if: "!enableGeolocation",
delete_properties: ["geolocation"],
}
The WrapHelper API
A WrapHelper
object is globally available to wrappers code, exposing some methods and properties which are mostly
used internally by the code builders to automate tasks such as handling Firefox's content script sandbox or making replacement objects look as native as possible.
However a few of them may be useful to complex wrappers or in edge case not covered by callable_name
and other declarative object replacement / property definition wrapper constructs:
WrapHelper.shared: {}
- a "bare" JavaScript object which a wrapper can use to share information with other wrappers (e.g. to coordinate behavior between related parts of the same API requiring multiple wrappings) by attaching its own data objects as properties. Warning: namespacing is not enforced and up to the wrapper implementor, but obviously recommended.WrapHelper.overlay(obj, data)
- Proxies the prototype of theobj
object in order to return the properties of thedata
object as if they were native properties (e.g. as if they were returned by getters on the prototype chain, rather than defined on the instance). This allows spoofing some native objects data in a less detectable / fingerprintable way than by usingObject.defineProperty()
. SeewrappingS-MCS.js
for an example.WrapHelper.forPage(obj)
- it's mostly used internally and transparently bycode_builder.js
, but may be useful to very complex proxies in edge cases needing to explicitly prepare an object/function created in Firefox's sandboxed content script environment to be consumed/called from the page context, and to make replacements for native objects and functions provided by the wrappers look as much native as possible (on Chromium, too). In most cases, however, this gets automated by the code builders replacing Object methods with their WrapHelper counterpart in the wrapper sources and by proxying "callable_name" function references throughWrapHelper.pageAPI()
(see below).WrapHelper.pageAPI(f)
- proxies the function/method f so that arguments and return values, and especially callbacks andPromise
objects, are recursively managed in order to transparently marshal objects back and forth [Firefox's sandbox for extensions (https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts). Wrapper implementors should almost never need to use this API directly, since any function referenced via its "callable_name" goes automatically through it.
The generated wrapper structure
To get a better idea how the code is generated see the following pseudo code. Please do not refer to any name created by the code builders from your wrapper. Use your custom names. If it is not available, please, open an issue where you explain what you are trying to achieve. It is probable that we will introduce a new property that allows to provide your name to the code builders. The code lives in an anonymous namespaces, so variables introduced here do not directly leak to page scripts.
// Define wrapped_name(s) variables holding the original JS objects
helping_code // if present
function wrapping_code_function_name(param) {
// Store original function: either original_function which defaults to parent_object.parent_object_property
function replacement(wrapping_function_args) {
wrapping_function_body
}
if (replace_original_function)
original_function = replacement
else
parent_object.parent_object_property = replacement
// Modify toString
post_replacement_code
}
wrapping_code_function_name(window if wrapping_code_function_call_window) // The function is called even if the name is not explicitely provided
// wrappings generated by post wrapping code
Compiling the wrappers
Of course the wrappers need to be compiled to JavaScript before inserting the code to page scripts. See code_builders.js
.
Registering a new wrapper
fix_manifest.sh
automatically adds all modules with file name of wrapping*.js
to the manifest.json of the extension. There is no need for any additional action.
Register new wrapper to be used by the extension in a level or available in the GUI.
See levels.js
and its list wrapping_groups.groups
. Once you add your wrapper to an existing group or create a new group, the wrapper becomes available in the built-in levels containing the group and in the GUI for custom levels.
Describe the wrapper for Doxygen documentation
Describe what the wrapper tries to accomplish and its approach:
- Add Doxygen comment at the top of the file with the following structure:
/** \file
* \brief This file contains wrappers for the X API (link to the standard or MDN)
* \ingroup wrappers
*
* Put at least one paragraph describing the goal of the wrapper. Describe a possible attack vector.
* Link to further resources/readings.
*
* Describe the options of the wrapper. Mention examples when a user should want to use the
* available options.
*
* Describe any \note, \bug ór use other suitable Doxygen commands
* (https://www.doxygen.nl/manual/commands.html)
*/
- Describe helping functions and
wrapping_function_body
- Use Doxygen command
\fn fake wrapped.object
for eachwrapping_function_body
- Use the following structure for function comments:
/**
* \brief Short description
*
* \param X Descripton
* \param Y Descripton
*
* \returns Description
*
* Main description of the aim of the function, algorithm, etc.
*
* Describe any \note, \bug ór use other suitable Doxygen commands
* (https://www.doxygen.nl/manual/commands.html)
*/
Write unit tests or integration tests for the wrapper
Follow instructions for unit testing and integration testing.