CVE-2022-24762 (published 2022-03-14T23:15Z)

sysend.js is a library that allows a user to send messages between pages that are open in the same browser. Users that use cross-origin communication may have their communications intercepted. Impact is limited by the communication occurring in the same browser. This issue has been patched in sysend.js version 1.10.0. The only currently known workaround is to avoid sending communications that a user does not want to have intercepted via sysend messages.

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-346
cwe_name: Origin Validation Error
description: The product does not properly verify that the source of data or communication is valid.
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/jcubic/sysend.js
commit_details: repo_url: https://github.com/jcubic/sysend.js
author: Jakub Jankiewicz
author_date: 2022-03-13 14:21:22+01:00
msg: fix security issue with Cross-Domain communication #33
repository: repo_url: https://github.com/jcubic/sysend.js
repo_name: jcubic/sysend.js
description: Web application synchronization between different tabs
repo_language: JavaScript
owner: jcubic
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -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:
@@ -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
 
@@ -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:
@@ -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).
 

programming_language: Markdown
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -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",

programming_language: JavaScript
filename: proxy.html
change_type: ModificationType.MODIFY
diff: @@ -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>

programming_language: HTML
filename: sysend.d.ts
change_type: ModificationType.MODIFY
diff: @@ -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
@@ -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;
 }
 

programming_language: TypeScript
filename: sysend.js
change_type: ModificationType.MODIFY
diff: @@ -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
@@ -41,6 +41,7 @@
     // id of the window/tab
     var target_id = generate_uuid();
     var target_count = 1;
+    var domains;
 
     var handlers = {
         primary: [],
@@ -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';
@@ -176,6 +179,9 @@
                 });
             });
         },
+        channel: function() {
+            domains = [].slice.apply(arguments).map(origin);
+        },
         isPrimary: function() {
             return primary;
         }
@@ -207,8 +213,6 @@
         };
     })();
     // -------------------------------------------------------------------------
-    init();
-    // -------------------------------------------------------------------------
     function delay(time) {
         return function() {
             return new Promise(function(resolve) {
@@ -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;
@@ -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
@@ -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
@@ -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), "*");
+            }
         });
     }
     // -------------------------------------------------------------------------
@@ -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
@@ -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) {
@@ -448,7 +513,6 @@
             init_visiblity();
 
             sysend.track('visbility', function(visible) {
-                console.log({visible, has_primary});
                 if (visible && !has_primary) {
                     become_primary();
                 }
@@ -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;

programming_language: JavaScript