diff --git a/README.md b/README.md index 4811957..f378226 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,79 @@ If key authentication fails, check: For additional support or troubleshooting, please open an issue on the GitHub repository. +### Environment Variables via URL + +WebSSH2 supports passing environment variables through URL parameters, allowing you to customize the SSH session environment. This feature enables scenarios like automatically opening specific files or setting custom environment variables. + +#### Server Configuration + +Before using this feature, you must configure your SSH server to accept the environment variables you want to pass. Edit your `/etc/ssh/sshd_config` file to include the desired variables in the `AcceptEnv` directive: + +```bash +# Allow client to pass locale environment variables and custom vars +AcceptEnv LANG LC_* VIM_FILE CUSTOM_ENV +``` + +Remember to restart your SSH server after making changes: +```bash +sudo systemctl restart sshd # For systemd-based systems +# or +sudo service sshd restart # For init.d-based systems +``` + +#### Usage + +Pass environment variables using the `env` query parameter: + +```bash +# Single environment variable +http://localhost:2222/ssh/host/example.com?env=VIM_FILE:config.txt + +# Multiple environment variables +http://localhost:2222/ssh/host/example.com?env=VIM_FILE:config.txt,CUSTOM_ENV:test +``` + +#### Security Considerations + +To maintain security, environment variables must meet these criteria: + +- Variable names must: + - Start with a capital letter + - Contain only uppercase letters, numbers, and underscores + - Be listed in the SSH server's `AcceptEnv` directive +- Variable values cannot contain shell special characters (;, &, |, `, $) + +Invalid environment variables will be silently ignored. + +#### Example Usage + +1. Configure your SSH server as shown above. + +2. Create a URL with environment variables: + ``` + http://localhost:2222/ssh/host/example.com?env=VIM_FILE:settings.conf,CUSTOM_ENV:production + ``` + +3. In your remote server's `.bashrc` or shell initialization file: + ```bash + if [ ! -z "$VIM_FILE" ]; then + vim "$VIM_FILE" + fi + + if [ ! -z "$CUSTOM_ENV" ]; then + echo "Running in $CUSTOM_ENV environment" + fi + ``` + +#### Troubleshooting + +If environment variables aren't being set: + +1. Verify the variables are permitted in `/etc/ssh/sshd_config` +2. Check SSH server logs for any related errors +3. Ensure variable names and values meet the security requirements +4. Test with a simple variable first to isolate any issues + ## Routes WebSSH2 provides two main routes: diff --git a/app/routes.js b/app/routes.js index ea317b9..468a1da 100644 --- a/app/routes.js +++ b/app/routes.js @@ -14,6 +14,7 @@ const { createNamespacedDebug } = require("./logger") const { createAuthMiddleware } = require("./middleware") const { ConfigError, handleError } = require("./errors") const { HTTP } = require("./constants") +const { parseEnvVars } = require("./utils") const debug = createNamespacedDebug("routes") @@ -43,6 +44,11 @@ module.exports = function(config) { */ router.get("/host/", auth, (req, res) => { debug(`router.get.host: /ssh/host/ route`) + const envVars = parseEnvVars(req.query.env) + if (envVars) { + req.session.envVars = envVars + debug("routes: Parsed environment variables: %O", envVars) + } try { if (!config.ssh.host) { @@ -76,8 +82,13 @@ module.exports = function(config) { }) // Scenario 2: Auth required, uses HTTP Basic Auth - router.get("/host/:host", auth, (req, res) => { + router.get("/host/:host?", auth, (req, res) => { debug(`router.get.host: /ssh/host/${req.params.host} route`) + const envVars = parseEnvVars(req.query.env) + if (envVars) { + req.session.envVars = envVars + debug("routes: Parsed environment variables: %O", envVars) + } try { const host = getValidatedHost(req.params.host) diff --git a/app/socket.js b/app/socket.js index e9beba9..5448ba9 100644 --- a/app/socket.js +++ b/app/socket.js @@ -206,14 +206,20 @@ class WebSSH2Socket extends EventEmitter { * Creates a new SSH shell session. */ createShell() { + // Get envVars from socket session if they exist + const envVars = this.socket.handshake.session.envVars || null + this.ssh - .shell({ - term: this.sessionState.term, - cols: this.sessionState.cols, - rows: this.sessionState.rows - }) - .then(stream => { - stream.on("data", data => { + .shell( + { + term: this.sessionState.term, + cols: this.sessionState.cols, + rows: this.sessionState.rows + }, + envVars + ) + .then((stream) => { + stream.on("data", (data) => { this.socket.emit("data", data.toString("utf-8")) }) // stream.stderr.on("data", data => debug(`STDERR: ${data}`)) // needed for shell.exec diff --git a/app/ssh.js b/app/ssh.js index ec8ce29..5fd71f1 100644 --- a/app/ssh.js +++ b/app/ssh.js @@ -190,12 +190,17 @@ class SSHConnection extends EventEmitter { /** * Opens an interactive shell session over the SSH connection. - * @param {Object} [options] - Optional parameters for the shell. - * @returns {Promise} - A promise that resolves with the SSH shell stream. + * @param {Object} options - Options for the shell + * @param {Object} [envVars] - Environment variables to set + * @returns {Promise} - A promise that resolves with the SSH shell stream */ - shell(options) { + shell(options, envVars) { + const shellOptions = Object.assign({}, options, { + env: this.getEnvironment(envVars) + }) + return new Promise((resolve, reject) => { - this.conn.shell(options, (err, stream) => { + this.conn.shell(shellOptions, (err, stream) => { if (err) { reject(err) } else { @@ -230,6 +235,25 @@ class SSHConnection extends EventEmitter { this.conn = null } } + + /** + * Gets the environment variables for the SSH session + * @param {Object} envVars - Environment variables from URL + * @returns {Object} - Combined environment variables + */ + getEnvironment(envVars) { + const env = { + TERM: this.config.ssh.term + } + + if (envVars) { + Object.keys(envVars).forEach((key) => { + env[key] = envVars[key] + }) + } + + return env + } } module.exports = SSHConnection diff --git a/app/utils.js b/app/utils.js index 3732a6b..5ab7ca3 100644 --- a/app/utils.js +++ b/app/utils.js @@ -198,13 +198,64 @@ function maskSensitiveData(obj, options) { return maskedObject } +/** + * Validates and sanitizes environment variable key names + * @param {string} key - The environment variable key to validate + * @returns {boolean} - Whether the key is valid + */ +function isValidEnvKey(key) { + // Only allow uppercase letters, numbers, and underscore + return /^[A-Z][A-Z0-9_]*$/.test(key) +} + +/** + * Validates and sanitizes environment variable values + * @param {string} value - The environment variable value to validate + * @returns {boolean} - Whether the value is valid + */ +function isValidEnvValue(value) { + // Disallow special characters that could be used for command injection + return !/[;&|`$]/.test(value) +} + +/** + * Parses and validates environment variables from URL query string + * @param {string} envString - The environment string from URL query + * @returns {Object|null} - Object containing validated env vars or null if invalid + */ +function parseEnvVars(envString) { + if (!envString) return null + + const envVars = {} + const pairs = envString.split(",") + + for (let i = 0; i < pairs.length; i += 1) { + const pair = pairs[i].split(":") + if (pair.length !== 2) continue + + const key = pair[0].trim() + const value = pair[1].trim() + + if (isValidEnvKey(key) && isValidEnvValue(value)) { + envVars[key] = value + } else { + debug(`parseEnvVars: Invalid env var pair: ${key}:${value}`) + } + } + + return Object.keys(envVars).length > 0 ? envVars : null +} + module.exports = { deepMerge, getValidatedHost, getValidatedPort, isValidCredentials, + isValidEnvKey, + isValidEnvValue, maskSensitiveData, modifyHtml, + parseEnvVars, validateConfig, validateSshTerm }