From 16a27ce62aa4c495d9fdb766e77d25932929991e Mon Sep 17 00:00:00 2001 From: Bill Church Date: Sat, 23 Nov 2019 08:45:59 -0500 Subject: [PATCH] Pr/163 (#164) * Add configuration option to restrict connections to specified subnets Signed-off-by: Matt Oswalt * Remove accidentally included message Signed-off-by: Matt Oswalt * Move to cidr-matcher Signed-off-by: Matt Oswalt * feat: Add configuration option to restrict connections to specified subnets --- README.md | 2 ++ app/config.json.sample | 3 ++- app/package-lock.json | 53 ++++++++++++++++++++++++++++++++++++++++++ app/package.json | 3 ++- app/server/app.js | 4 +++- app/server/socket.js | 14 +++++++++++ 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ff06c62..81b26f2 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ docker run --name webssh2 -d -p 2222:2222 -v `pwd`/app/config.json:/usr/src/conf * **ssh.keepaliveCountMax** - _integer_ - How many consecutive, unanswered SSH-level keepalive packets that can be sent to the server before disconnection (similar to OpenSSH's ServerAliveCountMax config option). **Default:** 10. +* **allowedSubnets** - _array_ - A list of subnets that the server is allowed to connect to via SSH. An empty array means all subnets are permitted; no restriction. **Default:** empty array. + * **terminal.cursorBlink** - _boolean_ - Cursor blinks (true), does not (false) **Default:** true. * **terminal.scrollback** - _integer_ - Lines in the scrollback buffer. **Default:** 10000. diff --git a/app/config.json.sample b/app/config.json.sample index fe7adb2..6e540d0 100644 --- a/app/config.json.sample +++ b/app/config.json.sample @@ -16,7 +16,8 @@ "term": "xterm-color", "readyTimeout": 20000, "keepaliveInterval": 120000, - "keepaliveCountMax": 10 + "keepaliveCountMax": 10, + "allowedSubnets": [] }, "terminal": { "cursorBlink": true, diff --git a/app/package-lock.json b/app/package-lock.json index b159ce1..a5bc7f6 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -586,6 +586,11 @@ } } }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -1080,6 +1085,14 @@ "tslib": "^1.9.0" } }, + "cidr-matcher": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cidr-matcher/-/cidr-matcher-2.1.1.tgz", + "integrity": "sha512-QPJRz4HDQxpB8AZWEqd6ejVp+siArXh3u1MYaUFV85cd293StGSMb87jVe0z9gS92KsFwxCxjb3utO3e5HKHTw==", + "requires": { + "ip6addr": "^0.2.2" + } + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -3136,6 +3149,11 @@ } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, "fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -4737,6 +4755,15 @@ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, + "ip6addr": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ip6addr/-/ip6addr-0.2.3.tgz", + "integrity": "sha512-qA9DXRAUW+lT47/i/4+Q3GHPwZjGt/atby1FH/THN6GVATA6+Pjp2nztH7k6iKeil7hzYnBwfSsxjthlJ8lJKw==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.4.0" + } + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", @@ -5039,6 +5066,11 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5067,6 +5099,17 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "jsx-ast-utils": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", @@ -8451,6 +8494,16 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "vinyl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", diff --git a/app/package.json b/app/package.json index f152c3e..82545ab 100644 --- a/app/package.json +++ b/app/package.json @@ -41,7 +41,8 @@ "validator": "~12.0.0", "xterm-addon-fit": "^0.3.0", "xterm-addon-search": "^0.3.0", - "xterm-addon-web-links": "^0.2.1" + "xterm-addon-web-links": "^0.2.1", + "cidr-matcher": "2.1.1" }, "scripts": { "start": "node index.js", diff --git a/app/server/app.js b/app/server/app.js index d2ea822..5f0b490 100644 --- a/app/server/app.js +++ b/app/server/app.js @@ -28,7 +28,8 @@ let config = { term: 'xterm-color', readyTimeout: 20000, keepaliveInterval: 120000, - keepaliveCountMax: 10 + keepaliveCountMax: 10, + allowedSubnets: [] }, terminal: { cursorBlink: true, @@ -153,6 +154,7 @@ app.get('/ssh/host/:host?', function (req, res, next) { algorithms: config.algorithms, keepaliveInterval: config.ssh.keepaliveInterval, keepaliveCountMax: config.ssh.keepaliveCountMax, + allowedSubnets: config.ssh.allowedSubnets, term: (/^(([a-z]|[A-Z]|[0-9]|[!^(){}\-_~])+)?\w$/.test(req.query.sshterm) && req.query.sshterm) || config.ssh.term, terminal: { diff --git a/app/server/socket.js b/app/server/socket.js index 156f9e2..31e98d7 100644 --- a/app/server/socket.js +++ b/app/server/socket.js @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ 'use strict' /* jshint esversion: 6, asi: true, node: true */ // socket.js @@ -6,6 +7,7 @@ var debug = require('debug') var debugWebSSH2 = require('debug')('WebSSH2') var SSH = require('ssh2').Client +var CIDRMatcher = require('cidr-matcher'); // var fs = require('fs') // var hostkeys = JSON.parse(fs.readFileSync('./hostkeyhashes.json', 'utf8')) var termCols, termRows @@ -21,6 +23,18 @@ module.exports = function socket (socket) { socket.disconnect(true) return } + + // If configured, check that requsted host is in a permitted subnet + if ( (((socket.request.session || {}).ssh || {}).allowedSubnets || {}).length && ( socket.request.session.ssh.allowedSubnets.length > 0 ) ) { + var matcher = new CIDRMatcher(socket.request.session.ssh.allowedSubnets); + if (!matcher.contains(socket.request.session.ssh.host)) { + socket.emit('401 UNAUTHORIZED') + debugWebSSH2('SOCKET: Requested host outside configured subnets / REJECTED') + socket.disconnect(true) + return + } + } + var conn = new SSH() socket.on('geometry', function socketOnGeometry (cols, rows) { termCols = cols