diff --git a/docs/usercontent.md b/docs/usercontent.md new file mode 100644 index 0000000000..e54851dd0d --- /dev/null +++ b/docs/usercontent.md @@ -0,0 +1,27 @@ +# Usercontent + +While decryption itself is safe to be done without a sandbox, +letting the browser and user interact with the resulting data may be dangerous, +previously `usercontent.riot.im` was used to act as a sandbox on a different origin to close the attack surface, +it is now possible to do by using a combination of a sandboxed iframe and some code written into the app which consumes this SDK. + +Usercontent is an iframe sandbox target for allowing a user to safely download a decrypted attachment from a sandboxed origin where it cannot be used to XSS your riot session out from under you. + +Its function is to create an Object URL for the user/browser to use but bound to an origin different to that of the riot instance to protect against XSS. + +It exposes a function over a postMessage API, when sent an object with the matching fields to render a download link with the Object URL: + +```json5 +{ + "imgSrc": "", // the src of the image to display in the download link + "imgStyle": "", // the style to apply to the image + "style": "", // the style to apply to the download link + "download": "", // download attribute to pass to the tag + "textContent": "", // the text to put inside the download link + "blob": "", // the data blob to wrap in an object url and allow the user to download +} +``` + +If only imgSrc, imgStyle and style are passed then just update the existing link without overwriting other things about it. + +It is expected that this target be available at `usercontent/` relative to the root of the app, this can be seen in riot-web's webpack config. diff --git a/src/usercontent/index.html b/src/usercontent/index.html new file mode 100644 index 0000000000..90a0fe7c16 --- /dev/null +++ b/src/usercontent/index.html @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/usercontent/index.js b/src/usercontent/index.js new file mode 100644 index 0000000000..8e77f6860e --- /dev/null +++ b/src/usercontent/index.js @@ -0,0 +1,49 @@ +var params = window.location.search.substring(1).split('&'); +var lockOrigin; +for (var i = 0; i < params.length; ++i) { + var parts = params[i].split('='); + if (parts[0] === 'origin') lockOrigin = decodeURIComponent(parts[1]); +} + +function remoteRender(event) { + const data = event.data; + + const img = document.createElement("img"); + img.id = "img"; + img.src = data.imgSrc; + img.style = data.imgStyle; + + const a = document.createElement("a"); + a.id = "a"; + a.rel = "noopener"; + a.target = "_blank"; + a.download = data.download; + a.style = data.style; + a.style.fontFamily = "Arial, Helvetica, Sans-Serif"; + a.href = window.URL.createObjectURL(data.blob); + a.appendChild(img); + a.appendChild(document.createTextNode(data.textContent)); + + const body = document.body; + // Don't display scrollbars if the link takes more than one line to display. + body.style = "margin: 0px; overflow: hidden"; + body.appendChild(a); +} + +function remoteSetTint(event) { + const data = event.data; + + const img = document.getElementById("img"); + img.src = data.imgSrc; + img.style = data.imgStyle; + + const a = document.getElementById("a"); + a.style = data.style; +} + +window.onmessage = function(e) { + if (lockOrigin === undefined || e.origin === lockOrigin) { + if (e.data.blob) remoteRender(e); + else remoteSetTint(e); + } +};