diff --git a/config/default.yaml b/config/default.yaml index a73e164..b8496f4 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -23,4 +23,12 @@ demobot: # Upstream configuration. This should almost never change. upstreams: - name: vector - url: "https://scalar.vector.im/api" \ No newline at end of file + url: "https://scalar.vector.im/api" + +# IPs and CIDR ranges listed here will be blocked from being widgets. +# Note: Widgets may still be embedded with restricted content, although not through Dimension directly. +widgetBlacklist: +- 10.0.0.0/8 +- 172.16.0.0/12 +- 192.168.0.0/16 +- 127.0.0.0/8 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b0d72e9..49b2ee1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4441,6 +4441,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "netmask": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" + }, "ngx-modialog": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/ngx-modialog/-/ngx-modialog-3.0.3.tgz", @@ -6659,8 +6664,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "querystring-es3": { "version": "0.2.1", @@ -8425,7 +8429,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, "requires": { "punycode": "1.3.2", "querystring": "0.2.0" @@ -8434,8 +8437,7 @@ "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" } } }, diff --git a/package.json b/package.json index f7d9256..ccafe2b 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,12 @@ "lodash": "^4.17.4", "matrix-js-sdk": "^0.8.2", "moment": "^2.18.1", + "netmask": "^1.0.6", "random-string": "^0.2.0", "request": "^2.81.0", "sequelize": "^4.7.5", "sqlite3": "^3.1.9", + "url": "^0.11.0", "winston": "^2.3.1" }, "devDependencies": { diff --git a/src/Dimension.js b/src/Dimension.js index e8c9bf1..cc3bb1e 100644 --- a/src/Dimension.js +++ b/src/Dimension.js @@ -32,7 +32,7 @@ class Dimension { this._app.use(bodyParser.json()); // Register routes for angular app - this._app.get(['/riot', '/riot/*'], (req, res) => { + this._app.get(['/riot', '/riot/*', '/widget_wrapper', '/widget_wrapper/*'], (req, res) => { res.sendFile(path.join(__dirname, "..", "web-dist", "index.html")); }); diff --git a/src/DimensionApi.js b/src/DimensionApi.js index 7c8dc47..8d90fce 100644 --- a/src/DimensionApi.js +++ b/src/DimensionApi.js @@ -1,7 +1,12 @@ -var IntegrationImpl = require("./integration/impl/index"); -var Integrations = require("./integration/index"); -var _ = require("lodash"); -var log = require("./util/LogService"); +const IntegrationImpl = require("./integration/impl/index"); +const Integrations = require("./integration/index"); +const _ = require("lodash"); +const log = require("./util/LogService"); +const request = require("request"); +const dns = require("dns-then"); +const urlParse = require("url"); +const Netmask = require("netmask").Netmask; +const config = require("config"); /** * API handler for the Dimension API @@ -26,6 +31,74 @@ class DimensionApi { app.delete("/api/v1/dimension/integrations/:roomId/:type/:integrationType", this._removeIntegration.bind(this)); app.put("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._updateIntegrationState.bind(this)); app.get("/api/v1/dimension/integrations/:roomId/:type/:integrationType/state", this._getIntegrationState.bind(this)); + app.get("/api/v1/dimension/widgets/embeddable", this._checkEmbeddable.bind(this)); + } + + _checkEmbeddable(req, res) { + // Unauthed endpoint. + + var url = req.query.url; + var parts = urlParse.parse(url); + var processed = false; + + // Only allow http and https + if (parts.protocol !== "http:" && parts.protocol !== "https:") { + res.status(400).send({error: "Invalid request scheme " + parts.protocol, canEmbed: false}); + processed = true; + return; + } + + // Verify the address is permitted for widgets + var hostname = parts.hostname.split(":")[0]; + dns.resolve4(hostname).then(addresses => { + log.verbose("DimensionApi", "Hostname " + hostname + " resolves to " + addresses); + if (addresses.length == 0) { + res.status(400).send({error: "Unrecongized address", canEmbed: false}); + processed = true; + return; + } + for (var ipOrCidr of config.get("widgetBlacklist")) { + var block = new Netmask(ipOrCidr); + for (var address of addresses) { + if (block.contains(address)) { + res.status(400).send({error: "Address not allowed", canEmbed: false}); + processed = true; + return; + } + } + } + }, err => { + log.verbose("DimensionApi", "Error resolving host " + hostname); + log.verbose("DimensionApi", err); + + res.status(400).send({error: "DNS error", canEmbed: false}); + processed = true; + }).then(() => { + if (processed) return; + + // Verify that the content can actually be embedded (CORS) + request(url, (err, response) => { + if (err) { + log.verbose("DimensionApi", "Error contacting host " + hostname); + log.verbose("DimensionApi", err); + + res.status(400).send({error: "Host error", canEmbed: false}); + return; + } + + if (response.statusCode >= 200 && response.statusCode < 300) { + // 200 OK + var headers = response.headers; + var xFrameOptions = (headers['x-frame-options'] || '').toLowerCase(); + + if (xFrameOptions === 'sameorigin' || xFrameOptions === 'deny') { + res.status(400).send({error: "X-Frame-Options forbids embedding", canEmbed: false}); + } else res.status(200).send({canEmbed: true}); + } else { + res.status(400).send({error: "Unsuccessful status code: " + response.statusCode, canEmbed: false}); + } + }); + }); } _getIntegration(integrationConfig, roomId, scalarToken) { @@ -77,7 +150,7 @@ class DimensionApi { for (var toRemove of remove) { var idx = integrations.indexOf(toRemove); if (idx === -1) continue; - log.warn("DimensionApi", "Disabling integration " + toRemove.name +" due to an error encountered in setup"); + log.warn("DimensionApi", "Disabling integration " + toRemove.name + " due to an error encountered in setup"); integrations.splice(idx, 1); } diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 0758c49..7d42dab 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -24,6 +24,7 @@ import { IrcApiService } from "./shared/irc-api.service"; import { TravisCiConfigComponent } from "./configs/travisci/travisci-config.component"; import { CustomWidgetConfigComponent } from "./configs/widget/custom_widget/custom_widget-config.component"; import { MyFilterPipe } from "./shared/my-filter.pipe"; +import { WidgetWrapperComponent } from "./widget_wrapper/widget_wrapper.component"; @NgModule({ imports: [ @@ -49,6 +50,7 @@ import { MyFilterPipe } from "./shared/my-filter.pipe"; TravisCiConfigComponent, CustomWidgetConfigComponent, MyFilterPipe, + WidgetWrapperComponent, // Vendor ], @@ -57,6 +59,7 @@ import { MyFilterPipe } from "./shared/my-filter.pipe"; ScalarService, IntegrationService, IrcApiService, + {provide: Window, useValue: window}, // Vendor ], diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 2b671b1..681f0e8 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -1,10 +1,12 @@ import { RouterModule, Routes } from "@angular/router"; import { HomeComponent } from "./home/home.component"; import { RiotComponent } from "./riot/riot.component"; +import { WidgetWrapperComponent } from "./widget_wrapper/widget_wrapper.component"; const routes: Routes = [ {path: "", component: HomeComponent}, {path: "riot", component: RiotComponent}, + {path: "riot/widget_wrapper", component: WidgetWrapperComponent}, ]; export const routing = RouterModule.forRoot(routes); diff --git a/web/app/configs/widget/custom_widget/custom_widget-config.component.html b/web/app/configs/widget/custom_widget/custom_widget-config.component.html index d7a5e93..1429f5d 100644 --- a/web/app/configs/widget/custom_widget/custom_widget-config.component.html +++ b/web/app/configs/widget/custom_widget/custom_widget-config.component.html @@ -27,7 +27,7 @@ -
+
{{ widget.name || widget.url }} (added by {{ widget.ownerId }})