How to write a new wrapper

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 of add_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 the build_wrapping_code variable directly, use add_wrappers instead.
  • The module also provides common functions used by the wrappers, e.g. rounding_function and noise_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 and parent_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 if wrapper_prototype is defined to provide the object name to have the prototype changed. Finally, Object.freeze can be optionally called on parent_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 mention window 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 by wrapping_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 the wrapped_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 define callable_name if the object is passed to other code like Promise 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 returns Promise objects: invoking them through their callable_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 variable wrapped_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 by wrapper_prototype.
  • original_function if not provided, it defaults to parent_object.parent_object_property. This name is used for overloading the toString 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 both replacement function and post_wrapping_code
  • replace_original_function is used to control which function should be replaced
  • post_replacement_code Allows to provide additional code with the access to the original function and to the wrapped function
  • freeze if set to true 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 wrapped Date 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 the obj object in order to return the properties of the data 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 using Object.defineProperty(). See wrappingS-MCS.js for an example.
  • WrapHelper.forPage(obj) - it's mostly used internally and transparently by code_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 through WrapHelper.pageAPI() (see below).
  • WrapHelper.pageAPI(f) - proxies the function/method f so that arguments and return values, and especially callbacks and Promise 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 each wrapping_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.