Merge pull request #4046 from matrix-org/t3chguy/usercontent
Get rid of dependence on usercontent.riot.impull/21833/head
commit
b1b17a313e
|
@ -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 <a/> 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.
|
|
@ -26,7 +26,7 @@ import {decryptFile} from '../../../utils/DecryptFile';
|
|||
import Tinter from '../../../Tinter';
|
||||
import request from 'browser-request';
|
||||
import Modal from '../../../Modal';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
|
||||
// A cached tinted copy of require("../../../../res/img/download.svg")
|
||||
|
@ -94,84 +94,6 @@ Tinter.registerTintable(updateTintedDownloadImage);
|
|||
// The downside of using a second domain is that it complicates hosting,
|
||||
// the downside of using a sandboxed iframe is that the browers are overly
|
||||
// restrictive in what you are allowed to do with the generated URL.
|
||||
//
|
||||
// For now given how unusable the blobs generated in sandboxed iframes are we
|
||||
// default to using a renderer hosted on "usercontent.riot.im". This is
|
||||
// overridable so that people running their own version of the client can
|
||||
// choose a different renderer.
|
||||
//
|
||||
// To that end the current version of the blob generation is the following
|
||||
// html:
|
||||
//
|
||||
// <html><head><script>
|
||||
// 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]);
|
||||
// }
|
||||
// window.onmessage=function(e){
|
||||
// if (lockOrigin === undefined || e.origin === lockOrigin) eval("("+e.data.code+")")(e);
|
||||
// }
|
||||
// </script></head><body></body></html>
|
||||
//
|
||||
// This waits to receive a message event sent using the window.postMessage API.
|
||||
// When it receives the event it evals a javascript function in data.code and
|
||||
// runs the function passing the event as an argument. This version adds
|
||||
// support for a query parameter controlling the origin from which messages
|
||||
// will be processed as an extra layer of security (note that the default URL
|
||||
// is still 'v1' since it is backwards compatible).
|
||||
//
|
||||
// In particular it means that the rendering function can be written as a
|
||||
// ordinary javascript function which then is turned into a string using
|
||||
// toString().
|
||||
//
|
||||
const DEFAULT_CROSS_ORIGIN_RENDERER = "https://usercontent.riot.im/v1.html";
|
||||
|
||||
/**
|
||||
* Render the attachment inside the iframe.
|
||||
* We can't use imported libraries here so this has to be vanilla JS.
|
||||
*/
|
||||
function remoteRender(event) {
|
||||
const data = event.data;
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.id = "img";
|
||||
img.src = data.imgSrc;
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.id = "a";
|
||||
a.rel = data.rel;
|
||||
a.target = data.target;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tint inside the iframe.
|
||||
* We can't use imported libraries here so this has to be vanilla JS.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current CSS style for a DOMElement.
|
||||
|
@ -283,7 +205,6 @@ export default createReactClass({
|
|||
// will be inside the iframe so we wont be able to update
|
||||
// it directly.
|
||||
this._iframe.current.contentWindow.postMessage({
|
||||
code: remoteSetTint.toString(),
|
||||
imgSrc: tintedDownloadImageURL,
|
||||
style: computedStyle(this._dummyLink.current),
|
||||
}, "*");
|
||||
|
@ -306,7 +227,7 @@ export default createReactClass({
|
|||
// Wait for the user to click on the link before downloading
|
||||
// and decrypting the attachment.
|
||||
let decrypting = false;
|
||||
const decrypt = () => {
|
||||
const decrypt = (e) => {
|
||||
if (decrypting) {
|
||||
return false;
|
||||
}
|
||||
|
@ -323,16 +244,15 @@ export default createReactClass({
|
|||
});
|
||||
}).finally(() => {
|
||||
decrypting = false;
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MFileBody_download">
|
||||
<a href="javascript:void(0)" onClick={decrypt}>
|
||||
<AccessibleButton onClick={decrypt}>
|
||||
{ _t("Decrypt %(text)s", { text: text }) }
|
||||
</a>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
@ -341,7 +261,6 @@ export default createReactClass({
|
|||
// When the iframe loads we tell it to render a download link
|
||||
const onIframeLoad = (ev) => {
|
||||
ev.target.contentWindow.postMessage({
|
||||
code: remoteRender.toString(),
|
||||
imgSrc: tintedDownloadImageURL,
|
||||
style: computedStyle(this._dummyLink.current),
|
||||
blob: this.state.decryptedBlob,
|
||||
|
@ -349,19 +268,13 @@ export default createReactClass({
|
|||
// will have the correct name when the user tries to download it.
|
||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
rel: "noopener",
|
||||
target: "_blank",
|
||||
textContent: _t("Download %(text)s", { text: text }),
|
||||
}, "*");
|
||||
};
|
||||
|
||||
// If the attachment is encryped then put the link inside an iframe.
|
||||
let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
|
||||
const appConfig = SdkConfig.get();
|
||||
if (appConfig && appConfig.cross_origin_renderer_url) {
|
||||
renderer_url = appConfig.cross_origin_renderer_url;
|
||||
}
|
||||
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
|
||||
const url = "usercontent/"; // XXX: this path should probably be passed from the skin
|
||||
|
||||
// If the attachment is encrypted then put the link inside an iframe.
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MFileBody_download">
|
||||
|
@ -373,7 +286,11 @@ export default createReactClass({
|
|||
*/ }
|
||||
<a ref={this._dummyLink} />
|
||||
</div>
|
||||
<iframe src={renderer_url} onLoad={onIframeLoad} ref={this._iframe} />
|
||||
<iframe
|
||||
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
|
||||
onLoad={onIframeLoad}
|
||||
ref={this._iframe}
|
||||
sandbox="allow-scripts allow-downloads" />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<html>
|
||||
<head>
|
||||
<!--
|
||||
Hello! If you're reading this, perhaps you're wondering what this
|
||||
file is doing and why your Riot is using it.
|
||||
In short, this allows Riot to isolate potentially unsafe encrypted
|
||||
attachments into their own origin, away from your Riot.
|
||||
Stay curious!
|
||||
-->
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
|
@ -0,0 +1,49 @@
|
|||
const params = window.location.search.substring(1).split('&');
|
||||
let lockOrigin;
|
||||
for (let i = 0; i < params.length; ++i) {
|
||||
const 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 (e.origin === lockOrigin) {
|
||||
if (e.data.blob) remoteRender(e);
|
||||
else remoteSetTint(e);
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue