Browse Source

update helmet and apply explicit policies on it

pull/270/head
butlerx 4 years ago
parent
commit
8e976699c0
No known key found for this signature in database GPG Key ID: B37CA765BAA89170
  1. 56
      package.json
  2. 4
      src/client/wetty/term/confiruragtion/clipboard.ts
  3. 22
      src/main.ts
  4. 4
      src/server/command.ts
  5. 20
      src/server/socketServer.ts
  6. 25
      src/server/socketServer/security.ts
  7. 18
      src/shared/config.ts
  8. 4
      src/shared/defaults.ts
  9. 2
      src/shared/interfaces.ts
  10. 1357
      yarn.lock

56
package.json

@ -66,6 +66,7 @@
}, },
"exclude": [ "exclude": [
"src/server/**/*.ts", "src/server/**/*.ts",
"src/client/**/*.spec.ts",
"src/*.ts" "src/*.ts"
], ],
"plugins": [ "plugins": [
@ -97,63 +98,60 @@
"@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^5.11.2",
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.17.1", "express": "^4.17.1",
"express-winston": "^4.0.1", "express-winston": "^4.0.5",
"file-type": "^12.3.0", "file-type": "^12.3.0",
"fs-extra": "^8.1.0", "fs-extra": "^9.0.1",
"helmet": "^3.20.1", "helmet": "^4.1.0",
"json5": "^2.1.3", "json5": "^2.1.3",
"lodash": "^4.17.19", "lodash": "^4.17.20",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"node-sass-middleware": "^0.11.0",
"sass": "^1.26.10", "sass": "^1.26.10",
"serve-favicon": "^2.5.0", "serve-favicon": "^2.5.0",
"socket.io": "^2.2.0", "socket.io": "^2.3.0",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.3.0",
"toastify-js": "^1.6.1", "toastify-js": "^1.9.1",
"winston": "^3.2.1", "winston": "^3.3.3",
"xterm": "^4.8.1", "xterm": "^4.8.1",
"xterm-addon-fit": "^0.4.0", "xterm-addon-fit": "^0.4.0",
"yargs": "^14.0.0" "yargs": "^15.4.1"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.5", "@types/chai": "^4.2.5",
"@types/compression": "^1.0.1", "@types/compression": "^1.7.0",
"@types/express": "^4.17.1", "@types/express": "^4.17.8",
"@types/fs-extra": "^8.0.0", "@types/fs-extra": "^9.0.1",
"@types/helmet": "^0.0.47", "@types/helmet": "^0.0.48",
"@types/jsdom": "^12.2.4", "@types/jsdom": "^12.2.4",
"@types/lodash": "^4.14.138", "@types/lodash": "^4.14.161",
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"@types/morgan": "^1.7.37", "@types/morgan": "^1.7.37",
"@types/node": "^12.7.3", "@types/node": "^14.6.3",
"@types/node-sass-middleware": "^0.0.31", "@types/serve-favicon": "^2.5.0",
"@types/serve-favicon": "^2.2.31",
"@types/sinon": "^7.5.1", "@types/sinon": "^7.5.1",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.11",
"@types/socket.io-client": "^1.4.32", "@types/socket.io-client": "^1.4.33",
"@types/webpack-env": "^1.14.0",
"@types/winston": "^2.4.4", "@types/winston": "^2.4.4",
"@types/yargs": "^15.0.5", "@types/yargs": "^15.0.5",
"@typescript-eslint/eslint-plugin": "^2.5.0", "@typescript-eslint/eslint-plugin": "^2.5.0",
"@typescript-eslint/parser": "^2.5.0", "@typescript-eslint/parser": "^2.5.0",
"all-contributors-cli": "^6.17.0", "all-contributors-cli": "^6.17.2",
"chai": "^4.2.0", "chai": "^4.2.0",
"concurrently": "^5.2.0", "concurrently": "^5.2.0",
"eslint": "^7.6.0", "eslint": "^7.8.1",
"eslint-config-airbnb-base": "^14.2.0", "eslint-config-airbnb-base": "^14.2.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
"git-authors-cli": "^1.0.27", "git-authors-cli": "^1.0.28",
"husky": "^4.2.5", "husky": "^4.2.5",
"jsdom": "^15.2.1", "jsdom": "^15.2.1",
"lint-staged": "^10.2.11", "lint-staged": "^10.2.13",
"mocha": "^6.2.2", "mocha": "^6.2.2",
"nodemon": "^2.0.4", "nodemon": "^2.0.4",
"prettier": "^2.0.5", "prettier": "^2.1.1",
"sinon": "^7.5.0", "sinon": "^7.5.0",
"snowpack": "^2.7.5", "snowpack": "^2.10.1",
"typescript": "^3.9.7" "typescript": "^4.0.2"
}, },
"contributors": [ "contributors": [
"Krishna Srinivas <krishna.srinivas@gmail.com>", "Krishna Srinivas <krishna.srinivas@gmail.com>",

4
src/client/wetty/term/confiruragtion/clipboard.ts

@ -4,8 +4,8 @@
@returns boolean to indicate success or failure @returns boolean to indicate success or failure
*/ */
export function copySelected(text: string): boolean { export function copySelected(text: string): boolean {
if (window.clipboardData?.setData) { if ((window as any).clipboardData?.setData) {
window.clipboardData.setData('Text', text); (window as any).clipboardData.setData('Text', text);
return true; return true;
} }
if ( if (

22
src/main.ts

@ -78,8 +78,9 @@ const opts = yargs
description: 'command to run in shell', description: 'command to run in shell',
type: 'string', type: 'string',
}) })
.option('bypass-helmet', { .option('allow-iframe', {
description: 'disable helmet from placing security restrictions', description:
'Allow wetty to be embedded in an iframe, defaults to allowing same origin',
type: 'boolean', type: 'boolean',
}) })
.option('help', { .option('help', {
@ -90,20 +91,15 @@ const opts = yargs
.boolean('allow_discovery').argv; .boolean('allow_discovery').argv;
if (!opts.help) { if (!opts.help) {
(async () => { loadConfigFile(opts.conf)
const config = await loadConfigFile(opts.conf); .then(config => mergeCliConf(opts, config))
const conf = mergeCliConf(opts, config); .then(conf =>
startServer( startServer(conf.ssh, conf.server, conf.command, conf.forceSSH, conf.ssl),
conf.ssh, )
conf.server, .catch((err: Error) => {
conf.command,
conf.forceSSH,
conf.ssl,
).catch(err => {
logger.error(err); logger.error(err);
process.exitCode = 1; process.exitCode = 1;
}); });
})();
} else { } else {
yargs.showHelp(); yargs.showHelp();
process.exitCode = 0; process.exitCode = 0;

4
src/server/command.ts

@ -1,6 +1,6 @@
import url from 'url'; import url from 'url';
import { Socket } from 'socket.io'; import type { Socket } from 'socket.io';
import { SSH } from '../shared/interfaces.js'; import type { SSH } from '../shared/interfaces';
import { address } from './command/address.js'; import { address } from './command/address.js';
import { loginOptions } from './command/login.js'; import { loginOptions } from './command/login.js';
import { sshOptions } from './command/ssh.js'; import { sshOptions } from './command/ssh.js';

20
src/server/socketServer.ts

@ -1,6 +1,5 @@
import express from 'express'; import express from 'express';
import compression from 'compression'; import compression from 'compression';
import helmet from 'helmet';
import winston from 'express-winston'; import winston from 'express-winston';
import type { SSL, SSLBuffer, Server } from '../shared/interfaces.js'; import type { SSL, SSLBuffer, Server } from '../shared/interfaces.js';
@ -9,10 +8,11 @@ import { html } from './socketServer/html.js';
import { listen } from './socketServer/socket.js'; import { listen } from './socketServer/socket.js';
import { logger } from '../shared/logger.js'; import { logger } from '../shared/logger.js';
import { serveStatic, trim } from './socketServer/assets.js'; import { serveStatic, trim } from './socketServer/assets.js';
import { policies } from './socketServer/security.js';
import { loadSSL } from './socketServer/ssl.js'; import { loadSSL } from './socketServer/ssl.js';
export async function server( export async function server(
{ base, port, host, title, bypassHelmet }: Server, { base, port, host, title, allowIframe }: Server,
ssl?: SSL, ssl?: SSL,
): Promise<SocketIO.Server> { ): Promise<SocketIO.Server> {
const basePath = trim(base); const basePath = trim(base);
@ -24,6 +24,7 @@ export async function server(
}); });
const app = express(); const app = express();
const client = html(basePath, title);
app app
.use(`${basePath}/web_modules`, serveStatic('web_modules')) .use(`${basePath}/web_modules`, serveStatic('web_modules'))
.use(`${basePath}/assets`, serveStatic('assets')) .use(`${basePath}/assets`, serveStatic('assets'))
@ -31,17 +32,10 @@ export async function server(
.use(winston.logger(logger)) .use(winston.logger(logger))
.use(compression()) .use(compression())
.use(favicon) .use(favicon)
.use(redirect); .use(redirect)
.use(policies(allowIframe))
// Allow helmet to be bypassed. .get(basePath, client)
// Unfortunately, order matters with middleware .get(`${basePath}/ssh/:user`, client);
// which is why this is thrown in the middle
if (!bypassHelmet) {
app.use(helmet());
}
const client = html(basePath, title);
app.get(basePath, client).get(`${basePath}/ssh/:user`, client);
const sslBuffer: SSLBuffer = await loadSSL(ssl); const sslBuffer: SSLBuffer = await loadSSL(ssl);

25
src/server/socketServer/security.ts

@ -0,0 +1,25 @@
import helmet from 'helmet';
import type { Request, Response } from 'express';
export const policies = (allowIframe: boolean) => (
req: Request,
res: Response,
next: (err?: unknown) => void,
) => {
helmet({
frameguard: allowIframe ? false : { action: 'sameorigin' },
referrerPolicy: { policy: ['no-referrer-when-downgrade'] },
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
styleSrc: ["'self'", "'unsafe-inline'"],
fontSrc: ["'self'", 'data:'],
connectSrc: [
"'self'",
(req.protocol === 'http' ? 'ws://' : 'wss://') + req.get('host'),
],
},
},
})(req, res, next);
};

18
src/shared/config.ts

@ -2,6 +2,7 @@ import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import JSON5 from 'json5'; import JSON5 from 'json5';
import isUndefined from 'lodash/isUndefined.js'; import isUndefined from 'lodash/isUndefined.js';
import type { Arguments } from 'yargs';
import type { Config, SSH, Server, SSL } from './interfaces'; import type { Config, SSH, Server, SSL } from './interfaces';
import { import {
@ -11,7 +12,15 @@ import {
defaultCommand, defaultCommand,
} from './defaults.js'; } from './defaults.js';
type confValue = boolean | string | number | undefined | SSH | Server | SSL; type confValue =
| boolean
| string
| number
| undefined
| unknown
| SSH
| Server
| SSL;
/** /**
* Cast given value to boolean * Cast given value to boolean
* *
@ -92,10 +101,7 @@ const objectAssign = (
* @returns merged configuration * @returns merged configuration
* *
*/ */
export function mergeCliConf( export function mergeCliConf(opts: Arguments, config: Config): Config {
opts: Record<string, confValue>,
config: Config,
): Config {
const ssl = { const ssl = {
key: opts['ssl-key'], key: opts['ssl-key'],
cert: opts['ssl-cert'], cert: opts['ssl-cert'],
@ -116,7 +122,7 @@ export function mergeCliConf(
host: opts.host, host: opts.host,
port: opts.port, port: opts.port,
title: opts.title, title: opts.title,
bypassHelmet: opts['bypass-helmet'], allowIframe: opts['allow-iframe'],
}) as Server, }) as Server,
command: isUndefined(opts.command) ? config.command : `${opts.command}`, command: isUndefined(opts.command) ? config.command : `${opts.command}`,
forceSSH: isUndefined(opts['force-ssh']) forceSSH: isUndefined(opts['force-ssh'])

4
src/shared/defaults.ts

@ -1,4 +1,4 @@
import type { SSH, Server } from "./interfaces"; import type { SSH, Server } from './interfaces';
export const sshDefault: SSH = { export const sshDefault: SSH = {
user: process.env.SSHUSER || '', user: process.env.SSHUSER || '',
@ -15,7 +15,7 @@ export const serverDefault: Server = {
port: parseInt(process.env.PORT || '3000', 10), port: parseInt(process.env.PORT || '3000', 10),
host: '0.0.0.0', host: '0.0.0.0',
title: process.env.TITLE || 'WeTTy - The Web Terminal Emulator', title: process.env.TITLE || 'WeTTy - The Web Terminal Emulator',
bypassHelmet: false, allowIframe: false,
}; };
export const forceSSHDefault = process.env.FORCESSH === 'true' || false; export const forceSSHDefault = process.env.FORCESSH === 'true' || false;

2
src/shared/interfaces.ts

@ -25,7 +25,7 @@ export interface Server {
host: string; host: string;
title: string; title: string;
base: string; base: string;
bypassHelmet: boolean; allowIframe: boolean;
} }
export interface Config { export interface Config {

1357
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save