From d9985e731858726b41a96e9619730410bb20121e Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 19 Feb 2020 13:20:22 +0200 Subject: [PATCH] Enforce STS policies --- src/models/network.js | 20 ++++++++- src/plugins/irc-events/cap.js | 11 ++++- src/plugins/sts.js | 82 +++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/plugins/sts.js diff --git a/src/models/network.js b/src/models/network.js index 41cab75b..c6004090 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -6,6 +6,7 @@ const IrcFramework = require("irc-framework"); const Chan = require("./chan"); const Msg = require("./msg"); const Helper = require("../helper"); +const STSPolicies = require("../plugins/sts"); module.exports = Network; @@ -78,7 +79,7 @@ Network.prototype.validate = function(client) { this.username = cleanString(this.username) || "thelounge"; this.realname = cleanString(this.realname) || "The Lounge User"; this.password = cleanString(this.password); - this.host = cleanString(this.host); + this.host = cleanString(this.host).toLowerCase(); this.name = cleanString(this.name); if (!this.port) { @@ -124,6 +125,23 @@ Network.prototype.validate = function(client) { return false; } + const stsPolicy = STSPolicies.get(this.host); + + if (stsPolicy && !this.tls) { + this.channels[0].pushMessage( + client, + new Msg({ + type: Msg.Type.ERROR, + text: `${this.host} has an active strict transport security policy, will connect to port ${stsPolicy.port} over a secure connection.`, + }), + true + ); + + this.port = stsPolicy.port; + this.tls = true; + this.rejectUnauthorized = true; + } + return true; }; diff --git a/src/plugins/irc-events/cap.js b/src/plugins/irc-events/cap.js index b477b1ab..3b1d7c8b 100644 --- a/src/plugins/irc-events/cap.js +++ b/src/plugins/irc-events/cap.js @@ -1,6 +1,7 @@ "use strict"; const Msg = require("../../models/msg"); +const STSPolicies = require("../sts"); module.exports = function(irc, network) { const client = this; @@ -27,7 +28,13 @@ module.exports = function(irc, network) { }); if (isSecure) { - // TODO: store and update duration + const duration = parseInt(values.duration, 10); + + if (isNaN(duration)) { + return; + } + + STSPolicies.update(network.host, network.port, duration); } else { const port = parseInt(values.port, 10); @@ -38,7 +45,7 @@ module.exports = function(irc, network) { network.channels[0].pushMessage( client, new Msg({ - text: `Server sent a strict transport security policy, reconnecting to port ${port}…`, + text: `Server sent a strict transport security policy, reconnecting to ${network.host}:${port}…`, }), true ); diff --git a/src/plugins/sts.js b/src/plugins/sts.js new file mode 100644 index 00000000..6dadcc59 --- /dev/null +++ b/src/plugins/sts.js @@ -0,0 +1,82 @@ +"use strict"; + +const _ = require("lodash"); +const fs = require("fs"); +const path = require("path"); +const log = require("../log"); +const Helper = require("../helper"); + +class STSPolicies { + constructor() { + this.stsFile = path.join(Helper.getHomePath(), "sts-policies.json"); + this.policies = new Map(); + this.refresh = _.debounce(this.saveFile, 10000, {maxWait: 60000}); + + if (!fs.existsSync(this.stsFile)) { + return; + } + + const storedPolicies = JSON.parse(fs.readFileSync(this.stsFile, "utf-8")); + const now = Date.now(); + + storedPolicies.forEach((value) => { + if (value.expires > now) { + this.policies.set(value.host, { + port: value.port, + expires: value.expires, + }); + } + }); + } + + get(host) { + const policy = this.policies.get(host); + + if (typeof policy === "undefined") { + return null; + } + + if (policy.expires <= Date.now()) { + this.policies.delete(host); + this.refresh(); + return null; + } + + return policy; + } + + update(host, port, duration) { + if (duration > 0) { + this.policies.set(host, { + port: port, + expires: Date.now() + duration * 1000, + }); + } else { + this.policies.delete(host); + } + + this.refresh(); + } + + saveFile() { + const policiesToStore = []; + + this.policies.forEach((value, key) => { + policiesToStore.push({ + host: key, + port: value.port, + expires: value.expires, + }); + }); + + const file = JSON.stringify(policiesToStore, null, "\t"); + + fs.writeFile(this.stsFile, file, {flag: "w+"}, (err) => { + if (err) { + log.error("Failed to update STS policies file!", err); + } + }); + } +} + +module.exports = new STSPolicies();