Skip to content
Permalink
Browse files Browse the repository at this point in the history
fix security issue with Cross-Domain communication #33
  • Loading branch information
jcubic committed Mar 13, 2022
1 parent 6a1ada0 commit a24f4b7
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 57 deletions.
48 changes: 16 additions & 32 deletions README.md
Expand Up @@ -2,20 +2,20 @@
<img src="https://github.com/jcubic/sysend.js/blob/master/assets/logo.svg?raw=true" alt="Sysend.js logo"/>
</p>

[![npm](https://img.shields.io/badge/npm-1.9.0-blue.svg)](https://www.npmjs.com/package/sysend)
![bower](https://img.shields.io/badge/bower-1.9.0-yellow.svg)
[![npm](https://img.shields.io/badge/npm-1.10.0-blue.svg)](https://www.npmjs.com/package/sysend)
![bower](https://img.shields.io/badge/bower-1.10.0-yellow.svg)
![downloads](https://img.shields.io/npm/dt/sysend.svg)
[![jsdelivr](https://img.shields.io/jsdelivr/npm/hm/sysend)](https://www.jsdelivr.com/package/npm/sysend)

# [Web application synchronization between different tabs](https://github.com/jcubic/sysend.js/)

sysend.js is a small library that allows to send messages between pages that are
open in the same browser. It also supports Cross-Domain communication. The library doesn't have
any dependencies and uses the HTML5 LocalStorage API or BroadcastChannel API.
If your browser don't support BroadcastChannel (see [Can I Use](https://caniuse.com/#feat=broadcastchannel))
then you can send any object that can be serialized to JSON. With BroadcastChannel you can send any object
(it will not be serialized to string but the values are limited to the ones that can be copied by
the [structured cloning algorithm](https://html.spec.whatwg.org/multipage/structured-data.html#structured-clone)).
sysend.js is a small library that allows to send messages between pages that are open in the same
browser. It also supports Cross-Domain communication (Cross-Origin). The library doesn't have any
dependencies and uses the HTML5 LocalStorage API or BroadcastChannel API. If your browser don't
support BroadcastChannel (see [Can I Use](https://caniuse.com/#feat=broadcastchannel)) then you can
send any object that can be serialized to JSON. With BroadcastChannel you can send any object (it
will not be serialized to string but the values are limited to the ones that can be copied by the
[structured cloning algorithm](https://html.spec.whatwg.org/multipage/structured-data.html#structured-clone)).
You can also send empty notifications.

Tested on:
Expand All @@ -26,11 +26,10 @@ MacOS X El Captain: Safari 9, Chrome 56, Firefox 51

## Note about Safari 7+ and Cross-Domain communication

All cross-domain communication is disabled by default with Safari 7+.
Because of a feature that block 3rd party tracking for iframe, and any
iframe used for cross-domain communication runs in sandboxed environment.
That's why this library like any other solution for cross-domain comunication,
don't work on Safari.
All cross-domain communication is disabled by default with Safari 7+. Because of a feature that
block 3rd party tracking for iframe, and any iframe used for cross-domain communication runs in
sandboxed environment. That's why this library like any other solution for cross-domain
comunication, don't work on Safari.

## Installation

Expand Down Expand Up @@ -106,23 +105,6 @@ And here is [multiple window tracking demo](https://jcubic.pl/windows.html). Ope

![Screen capture of Operating System Windows dragging and moving around animation](https://github.com/jcubic/sysend.js/blob/master/assets/windows-demo.gif?raw=true)

## Cross-domain Commuication Security

The iframe communication proxy, allow attackers to listen to any events sent to the iframe.
All they need to do is to use this code on any domain to connect to sysend channel:

```javascript
sysend.proxy('https://jcubic.pl/');
window.addEventListener('message', (e) => {
console.log(e);
});
```

This can lead to potential leaking of sensitive information from the website.
As of now there are not soution how to secure sysend cross-domain communication channel.

**To protect your application, don't send any sensitive infromation with cross-domain communication!**

## API

sysend object:
Expand All @@ -137,9 +119,11 @@ sysend object:
| `emit(name, [, object])` | same as `broadcast()` but also invoke the even on same page | name - string - The name of the event<br>object - optional any data | 1.5.0 |
| `post(<window_id>, [, object])` | send any data to other window | window_id - string of the target window<br>object - any data | 1.6.0 |
| `list()` | returns a Promise of objects `{id:<UUID>, primary}` for other windows, you can use those to send a message with `post()` | NA | 1.6.0 |
| `track(event, callback)` | track inter window communication events | event - any of the strings: `"open"`, `"close"`, `"primary"`, <br>`"secondary"`, `"message"`<br>callback - different function depend on the event:<br>* `"message"` - `{data, origin}` - where data is anything the `post()` sends, and origin is `id` of the sender.<br>* `"open"` - `{count, primary, id}` when new window/tab is opened<br>* `"close"` - `{count, primary, id, self}` when window/tab is closed<br>* `"primary"` and `"secondary"` function has no arguments and is called when window/tab become secondary or primary.<br>* `"ready"` - event when tracking is ready. | 1.6.0 except `ready` - 1.9.0 |
| `track(event, callback)` | track inter window communication events | event - any of the strings: `"open"`, `"close"`, `"primary"`, <br>`"secondary"`, `"message"`<br>callback - different function depend on the event:<br>* `"message"` - `{data, origin}` - where data is anything the `post()` sends, and origin is `id` of the sender.<br>* `"open"` - `{count, primary, id}` when new window/tab is opened<br>* `"close"` - `{count, primary, id, self}` when window/tab is closed<br>* `"primary"` and `"secondary"` function has no arguments and is called when window/tab become secondary or primary.<br>* `"ready"` - event when tracking is ready. | 1.6.0 except `ready` - 1.10.0 |
| `untrack(event [,callback])` | remove sigle event listener all all listeners for a given event | event - any of the strings `'open'`, `'close'`, `'primary'`, `'secondary'`, or `'message'`. | 1.6.0 |
| `isPrimary()` | function returns true if window is primary (first open or last that remain) | NA | 1.6.0 |
| `channel()` | function restrict cross domain communication to only allowed domains. You need to call this function on proxy iframe to limit number of domains (origins) that can listen and send events. | any number of origins (e.g. 'http://localhost:8080' or 'https://jcubic.github.io') you can also use valid URL. | 1.10.0 |


To see details of using the API, see [demo.html source code](https://github.com/jcubic/sysend.js/blob/master/demo.html) or [TypeScript definition file](https://github.com/jcubic/sysend.js/blob/master/sysend.d.ts).

Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "sysend",
"version": "1.9.0",
"version": "1.10.0",
"description": "Web application synchronization between different tabs",
"main": "sysend.js",
"typings": "sysend.d.ts",
Expand Down
6 changes: 5 additions & 1 deletion proxy.html
Expand Up @@ -3,7 +3,11 @@
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex,nofollow"/>
<script src="https://cdn.jsdelivr.net/npm/sysend"></script>
<script src="./sysend.js"></script>
<!-- EDIT THIS FILE AND CALL sysend.channel() WITH ALLOWED DOMAINS -->
<script>
sysend.channel();
</script>
</head>
<body>
</body>
Expand Down
3 changes: 2 additions & 1 deletion sysend.d.ts
@@ -1,5 +1,5 @@
/**@license
* sysend.js - send messages between browser windows/tabs version 1.9.0
* sysend.js - send messages between browser windows/tabs version 1.10.0
*
* Copyright (C) 2014-2022 Jakub T. Jankiewicz <https://jcubic.pl/me>
* Released under the MIT license
Expand All @@ -23,6 +23,7 @@ interface Sysend {
untrack(event: 'open' | 'close' | 'primary' | 'secondary' | 'message', fn?: (input?: any) => void): void;
list(): Promise<Array<{ id: string, primary: boolean }>>;
post(target: string, data?: any): void;
channel(...domains: string[]): void;
isPrimary(): boolean;
}

Expand Down
108 changes: 86 additions & 22 deletions sysend.js
@@ -1,5 +1,5 @@
/**@license
* sysend.js - send messages between browser windows/tabs version 1.9.0
* sysend.js - send messages between browser windows/tabs version 1.10.0
*
* Copyright (C) 2014-2022 Jakub T. Jankiewicz <https://jcubic.pl/me>
* Released under the MIT license
Expand Down Expand Up @@ -41,6 +41,7 @@
// id of the window/tab
var target_id = generate_uuid();
var target_count = 1;
var domains;

var handlers = {
primary: [],
Expand Down Expand Up @@ -84,6 +85,8 @@
},
proxy: function(url) {
if (typeof url === 'string' && host(url) !== window.location.host) {
domains = domains || [];
domains.push(origin(url));
var iframe = document.createElement('iframe');
iframe.style.width = iframe.style.height = 0;
iframe.style.border = 'none';
Expand Down Expand Up @@ -176,6 +179,9 @@
});
});
},
channel: function() {
domains = [].slice.apply(arguments).map(origin);
},
isPrimary: function() {
return primary;
}
Expand Down Expand Up @@ -207,8 +213,6 @@
};
})();
// -------------------------------------------------------------------------
init();
// -------------------------------------------------------------------------
function delay(time) {
return function() {
return new Promise(function(resolve) {
Expand All @@ -217,13 +221,37 @@
};
}
// -------------------------------------------------------------------------
function onLoad() {
var origin = (function() {
var a = document.createElement('a');
return function origin(url) {
a.href = url;
return a.origin;
};
})();
// -------------------------------------------------------------------------
// :: show only single message of this kind
// -------------------------------------------------------------------------
var warn_messages = [];
function warn(message) {
if (!warn_messages.includes(message)) {
warn_messages.push(message);
if (console && console.warn) {
console.warn(message);
} else {
setTimeout(function() {
throw new Error(message);
}, 0);
}
}
}
// -------------------------------------------------------------------------
function on_load() {
return new Promise(function(resolve) {
window.addEventListener('load', resolve, true);
}).then(iframeLoaded);
}).then(iframe_loaded);
}
// -------------------------------------------------------------------------
function iframeLoaded() {
function iframe_loaded() {
var iframes = Array.from(document.querySelectorAll('iframe'));
return Promise.all(iframes.filter(function(iframe) {
return iframe.src;
Expand All @@ -239,8 +267,30 @@
// the number was pick by experimentation
}
// -------------------------------------------------------------------------
// ref: https://stackoverflow.com/a/8809472/387194
// license: Public Domain/MIT
// :: valid sysend message
// -------------------------------------------------------------------------
function is_sysend_post_message(e) {
return typeof e.data === 'string' && e.data.match(prefix_re);
}
// -------------------------------------------------------------------------
function is_valid_origin(origin) {
if (!domains) {
warn('Call sysend.channel() on iframe to restrict domains that can '+
'use sysend channel');
return true;
}
var valid = domains.includes(origin);
if (!valid) {
warn(origin + ' domain is not on the list of allowed '+
'domains use sysend.channel() on iframe to allow'+
' access to this domain');
}
return valid;
}
// -------------------------------------------------------------------------
// :: ref: https://stackoverflow.com/a/8809472/387194
// :: license: Public Domain/MIT
// -------------------------------------------------------------------------
function generate_uuid() {
var d = new Date().getTime();
//Time in microseconds since page-load or 0 if unsupported
Expand Down Expand Up @@ -295,20 +345,20 @@
}
return data;
} catch (e) {
console.warn(prefix_message + e.message);
warn(prefix_message + e.message);
}
};
}
// -------------------------------------------------------------------------
// ref: https://stackoverflow.com/a/326076/387194
// -------------------------------------------------------------------------
function is_iframe() {
var is_iframe = (function is_iframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
})();
// -------------------------------------------------------------------------
function send_to_iframes(key, data) {
// propagate events to iframes
Expand All @@ -318,7 +368,9 @@
key: key,
data: data
};
iframe.window.postMessage(JSON.stringify(payload), "*");
if (is_valid_origin(origin(iframe.node.src))) {
iframe.window.postMessage(JSON.stringify(payload), "*");
}
});
}
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -387,21 +439,34 @@
sysend.emit('__primary__');
}
// -------------------------------------------------------------------------
init();
// -------------------------------------------------------------------------
function init() {
if (typeof window.BroadcastChannel === 'function') {
channel = new window.BroadcastChannel(uniq_prefix);
channel.addEventListener('message', function(event) {
if (event.target.name === uniq_prefix) {
var key = event.data && event.data.name;
if (callbacks[key]) {
invoke(key, unserialize(event.data.data));
if (is_iframe) {
var payload = {
name: uniq_prefix,
data: event.data,
iframe_id: target_id
};
if (is_valid_origin(origin(document.referrer))) {
window.parent.postMessage(JSON.stringify(payload), "*");
}
} else {
var key = event.data && event.data.name;
if (callbacks[key]) {
invoke(key, unserialize(event.data.data));
}
}
}
});
} else if (is_private_mode()) {
console.warn('Your browser don\'t support localStorgage. ' +
'In Safari this is most of the time because ' +
'of "Private Browsing Mode"');
warn('Your browser don\'t support localStorgage. ' +
'In Safari this is most of the time because ' +
'of "Private Browsing Mode"');
} else {
// we need to clean up localStorage if broadcast called on unload
// because setTimeout will never fire, even setTimeout 0
Expand Down Expand Up @@ -429,9 +494,9 @@
}, false);
}

if (is_iframe()) {
if (is_iframe) {
window.addEventListener('message', function(e) {
if (typeof e.data === 'string' && e.data.match(prefix_re)) {
if (is_sysend_post_message(e) && is_valid_origin(e.origin)) {
try {
var payload = JSON.parse(e.data);
if (payload && payload.name === uniq_prefix) {
Expand All @@ -448,7 +513,6 @@
init_visiblity();

sysend.track('visbility', function(visible) {
console.log({visible, has_primary});
if (visible && !has_primary) {
become_primary();
}
Expand Down Expand Up @@ -518,7 +582,7 @@
sysend.emit('__close__', { id: target_id, wasPrimary: primary });
}, { capture: true });

onLoad().then(function() {
on_load().then(function() {
sysend.list().then(function(list) {
target_count = list.length;
primary = list.length === 0;
Expand Down

0 comments on commit a24f4b7

Please sign in to comment.