Skip to content

Latest commit

 

History

History
256 lines (208 loc) · 9.5 KB

security.md

File metadata and controls

256 lines (208 loc) · 9.5 KB

Security

Table of Contents

Anatomy of an Endpoint.js Network

Endpoint.js uses Zone Routing Protocol to transfer messages throughout an ad-hoc overlay network between windows, web workers, clients, and servers. Intra-zone routing (or IARP) uses a modified, proactive Destination-Sequenced Distance Vector, or DSDV, routing protocol to discover and establish a routing table between hosts in the 'internal' network. Internal addressing is done using a unique UUID value generated every time an Endpoint.js node starts up. Inter-zone routing (or IERP) uses a reactive path-vector based algorithm to establish routes between hosts in the 'external' network. External addressing is done using a random UUID value generated by the 'server' host (in the client/server relationship). Discovering and building routes can only be done through a message passed via the Endpoint.js Bus.

Standard communication between windows, iframes, and web workers on the same origin is considered an 'internal' network. Client to server connections are considered 'external' connections, as well as connections between windows using cross-origin PostMessage communication.

Effort has gone into securing Endpoint.js from malicious code execution, spoofing, and poisoning attacks, as well as memory flooding attacks. However, as Endpoint.js Facade/Adapter connections are stateful, it still suffers from general Denial of Service attack vectors. It is recommended that access be restricted to authenticated, authorized users to help prevent these attacks.

Limiting Discovery

Definitions:

  • Local: Within the single Endpoint.js instance
  • Group: Within all Endpoint.js instances considered 'internal', or within the same IARP routing network.
  • Global: To all internal nodes, and immediate 'external' or border networks.
  • Universal: To all nodes and networks, including those reachable by my neighbors

Adapters can specify whether they want to respond to local, group (default) or global/universal messages. This prevents unauthorized clients from accessing protected functionality. By default, Adapters respond to Local or Group requests. If a message originated from an external node, then it is considered a global/universal message. There is no way of discriminating between a global or universal message because there is no trust relationship between external nodes.

Setting the neighborhood on an adapter. It will only respond to local facades:

var settings = {
    neighborhood: 'local'
};
var adapter = endpoint.createAdapter('chat-api', '1.0', chatApi, settings);

Facades can specify whether they want to broadcast on local(default), group, global or universal. If global is specified, then the message will be sent to an external node with the request that it not be redistributed. If the external node is malicious, it could send the message to an external node (thus making it universal). If universal is specified, then the message will be sent universally to every internal and external node in the network. It is recommended that this is used sparingly due to performance considerations.

See below on how to set the neighborhood on a query or facade. They will search for any adapter within the group:

var settings = {
    neighborhood: 'group'
};
var query = endpoint.createQuery('chat-api', '1.0', settings);
var facade = endpoint.createFacade('chat-api', '1.0', settings);

Valid values are 'local', 'group', 'global' and 'universal'.

Bridging Links

By default, Endpoint.js will not operate as a relay for external traffic. This functionality must be explicitly enabled by creating a bridge.

To create a bridge, use the configuration API:

var bridge = endpoint.getConfiguration().createBridge(['external-interface-1', 'internal-interface-1']);

Additional links can be added or removed from the bridge after it has been created, or it can be closed:

bridge.addLinkId('trusted-server');
bridge.removeLinkId('trusted-server');
bridge.close();

The 'createBridge' function supports a second argument allowing Endpoint.js to relay traffic between connections on the link itself:

var bridge = endpoint.getConfiguration().createBridge(['external-interface'], true);

Internal links will always relay to each other.

Limiting Bus Events

Bridges can be used to limit which links receive bus events. This can be useful if you want to ensure that requests for certain external APIs are only fulfilled by a certain interface.

The following example shows using the bridge to query and create a facade on a specific interface only:

var bridge = endpoint.getConfiguration().createBridge(['external-interface']);

var settings = {
    bridgeId: bridge.getId()
};

// Create a query
var query = endpoint.createQuery('chat-api', '1.0', settings);

// Create a facade
var facade = endpoint.createFacade('chat-api', '1.0', settings);

Client and Server Communication

By default, 'server' connections are considered external to protect internal networks. However, you may wish to setup an internal network of nodes to take advantage of a cloud architecture or load balance. To create an internal network, you can specify the 'external' flag when creating your link:

var internalLink = {
    linkId: 'internal-server',
    type: 'server',
    settings {
        external: false
    }
};

You must remember to host this link off of a different socket.io instance as one hosting external links. See the app.js file in the examples/ folder for an example of this.

Cross-Origin in the Browser

By default, additional 'window' links (beyond the built in one) are considered external to protect internal networks.

To add a cross-domain window link, you must specify the origin:

var crossDomainLink = {
    linkId: 'cross-domain-xyz.com',
    type: 'window',
    settings {
        external: true,
        origin: 'xyz.com'
    }
};

Stream Transformation

All underlying connections in Endpoint.js are stream based. This means that transformations can be applied, traffic can be interdicted, logged, encrypted, or digitally signed.

To add a stream transformer, specify the 'transformerFactory' option when creating/adding the link. This function should take one parameter, an instance of LinkTransform, which allows the user to add new transformations.

Creating the link:

var transformFactory = function(transform) {
    var readFunc = function(chunk, enc, cb) {
        console.log('reading message %j', chunk);
        this.push(chunk);
        cb();
    };
    var writeFunc = function(chunk, enc, cb) {
        console.log('writing message %j', chunk);
        this.push(chunk);
        cb();
    };
    transform.addTransform(readFunc, writeFunc);
};

var link = {
    linkId: 'id',
    type: 'server',
    settings: {
        transformFactory: transformFactory
    }
};

You can also use Node.js streams in the arguments to addTransform(). The 'chunk' values will be JSON objects, so if encrypting or decrypting, you may have to apply a transform.

Encryption and Digital Signatures

You can directly use the Node.js crypto package to digitally sign messages before they are sent to another node. Transform streams are considered Object streams, so you should perform an operation to 'stringify' (See JSON.stringify) the data before signing it.

PIN Authorization

To authorize cross-domain communication, you may wish to create a 'pin' based authentication mechanism. In this type of authorization model, a window pops up with a pin value, requesting that the value be copied and pasted into another window on the opposite window. Once that value is specified, it is passed through the link and verified, letting traffic pass.

A basic conceptual example (untested) follows:

Pin generation function:

var pin;
var pinGenerated = false;
var queuedMessage = null;
var verified = false;

var pinRead = function(chunk, enc, cb) {
    if (verified) {
        this.push(chunk);
        cb();
    }
    else {
        if (!pinGenerated) {
            pin = '12345';
            pinGenerated = true;
            createPinDialog(pin);
        }
        if (chunk.pin) {
            if (chunk.pin == pin) {
                verified = true;
                if (queuedMessage) {
                    this.push(queuedMessage);
                }
                cb();
            }
        }
        else {
            queuedMessage = chunk;
        }
    }
};

Pin verification function:

var pinWrite = function(chunk, enc, cb) {
    var _this = this;
    uiEvent.on('pin-ready', function(pin)) {
        _this.push({
            pin: pin
        });
    });
    this.push(chunk);
    cb();
};