From 1e4d64d300eb4d1cb15f7f2dca9aee720b0c0e98 Mon Sep 17 00:00:00 2001
From: Tuomas Haikarainen <tuomas.haikarainen@wapice.com>
Date: Thu, 7 Jul 2022 11:37:31 +0300
Subject: [PATCH] Added permitopen option to authorized_keys

It is now possible to limit local port forwarding to specified
destination(s) by using the permitopen option in authorized_keys.

Resolves #181
---
 auth.h                  | 13 +++++++
 dropbear.8              |  8 ++++
 svr-authpubkeyoptions.c | 81 +++++++++++++++++++++++++++++++++++++++++
 svr-tcpfwd.c            |  5 +++
 4 files changed, 107 insertions(+)

diff --git a/auth.h b/auth.h
index 2063cad..264c1f1 100644
--- a/auth.h
+++ b/auth.h
@@ -28,6 +28,7 @@
 #include "includes.h"
 #include "signkey.h"
 #include "chansession.h"
+#include "list.h"
 
 void svr_authinitialise(void);
 
@@ -45,6 +46,7 @@ int svr_pubkey_allows_agentfwd(void);
 int svr_pubkey_allows_tcpfwd(void);
 int svr_pubkey_allows_x11fwd(void);
 int svr_pubkey_allows_pty(void);
+int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port);
 void svr_pubkey_set_forced_command(struct ChanSess *chansess);
 void svr_pubkey_options_cleanup(void);
 int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filename);
@@ -54,6 +56,9 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena
 #define svr_pubkey_allows_tcpfwd() 1
 #define svr_pubkey_allows_x11fwd() 1
 #define svr_pubkey_allows_pty() 1
+static inline int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port)
+	{ (void)host; (void)port; return 1; }
+
 static inline void svr_pubkey_set_forced_command(struct ChanSess *chansess) { }
 static inline void svr_pubkey_options_cleanup(void) { }
 #define svr_add_pubkey_options(x,y,z) DROPBEAR_SUCCESS
@@ -93,6 +98,7 @@ void cli_auth_pubkey_cleanup(void);
 #define AUTH_METHOD_INTERACT "keyboard-interactive"
 #define AUTH_METHOD_INTERACT_LEN 20
 
+#define PUBKEY_OPTIONS_ANY_PORT UINT_MAX
 
 
 /* This structure is shared between server and client - it contains
@@ -139,6 +145,13 @@ struct PubKeyOptions {
 	int no_pty_flag;
 	/* "command=" option. */
 	char * forced_command;
+	/* "permitopen=" option */
+	m_list *permit_open_destinations;
+};
+
+struct PermitTCPFwdEntry {
+	char *host;
+	unsigned int port;
 };
 #endif
 
diff --git a/dropbear.8 b/dropbear.8
index 3073dd0..30df631 100644
--- a/dropbear.8
+++ b/dropbear.8
@@ -144,6 +144,14 @@ same functionality with other means even if no-pty is set.
 .B restrict
 Applies all the no- restrictions listed above.
 
+.TP
+.B permitopen=\fR"\fIhost:port\fR"
+Restrict local port forwarding so that connection is allowed only to the
+specified host and port. Multiple permitopen options separated by commas
+can be set in authorized_keys. Wildcard character ('*') may be used in
+port specification for matching any port. Hosts must be literal domain names or
+IP addresses.
+
 .TP
 .B command=\fR"\fIforced_command\fR"
 Disregard the command provided by the user and always run \fIforced_command\fR.
diff --git a/svr-authpubkeyoptions.c b/svr-authpubkeyoptions.c
index 447f4b7..e25bcf1 100644
--- a/svr-authpubkeyoptions.c
+++ b/svr-authpubkeyoptions.c
@@ -46,6 +46,7 @@
 #include "dbutil.h"
 #include "signkey.h"
 #include "auth.h"
+#include "runopts.h"
 
 #if DROPBEAR_SVR_PUBKEY_OPTIONS_BUILT
 
@@ -88,6 +89,29 @@ int svr_pubkey_allows_pty() {
 	return 1;
 }
 
+/* Returns 1 if pubkey allows local tcp fowarding to the provided destination,
+ * 0 otherwise */
+int svr_pubkey_allows_local_tcpfwd(const char *host, unsigned int port) {
+	if (ses.authstate.pubkey_options
+		&& ses.authstate.pubkey_options->permit_open_destinations) {
+		m_list_elem *iter = ses.authstate.pubkey_options->permit_open_destinations->first;
+		while (iter) {
+			struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)iter->item;
+			if (strcmp(entry->host, host) == 0) {
+				if ((entry->port == PUBKEY_OPTIONS_ANY_PORT) || (entry->port == port)) {
+					return 1;
+				}
+			}
+
+			iter = iter->next;
+		}
+
+		return 0;
+	}
+
+	return 1;
+}
+
 /* Set chansession command to the one forced 
  * by any 'command' public key option. */
 void svr_pubkey_set_forced_command(struct ChanSess *chansess) {
@@ -113,6 +137,16 @@ void svr_pubkey_options_cleanup() {
 		if (ses.authstate.pubkey_options->forced_command) {
 			m_free(ses.authstate.pubkey_options->forced_command);
 		}
+		if (ses.authstate.pubkey_options->permit_open_destinations) {
+			m_list_elem *iter = ses.authstate.pubkey_options->permit_open_destinations->first;
+			while (iter) {
+				struct PermitTCPFwdEntry *entry = (struct PermitTCPFwdEntry*)list_remove(iter);
+				m_free(entry->host);
+				m_free(entry);
+				iter = ses.authstate.pubkey_options->permit_open_destinations->first;
+			}
+			m_free(ses.authstate.pubkey_options->permit_open_destinations);
+		}
 		m_free(ses.authstate.pubkey_options);
 	}
 	if (ses.authstate.pubkey_info) {
@@ -205,6 +239,53 @@ int svr_add_pubkey_options(buffer *options_buf, int line_num, const char* filena
 			dropbear_log(LOG_WARNING, "Badly formatted command= authorized_keys option");
 			goto bad_option;
 		}
+		if (match_option(options_buf, "permitopen=\"") == DROPBEAR_SUCCESS) {
+			int valid_option = 0;
+			const unsigned char* permitopen_start = buf_getptr(options_buf, 0);
+
+			if (!ses.authstate.pubkey_options->permit_open_destinations) {
+				ses.authstate.pubkey_options->permit_open_destinations = list_new();
+			}
+
+			while (options_buf->pos < options_buf->len) {
+				const char c = buf_getbyte(options_buf);
+				if (c == '"') {
+					char *spec = NULL;
+					char *portstring = NULL;
+					const int permitopen_len = buf_getptr(options_buf, 0) - permitopen_start;
+					struct PermitTCPFwdEntry *entry =
+							(struct PermitTCPFwdEntry*)m_malloc(sizeof(struct PermitTCPFwdEntry));
+
+					list_append(ses.authstate.pubkey_options->permit_open_destinations, entry);
+					spec = m_malloc(permitopen_len);
+					memcpy(spec, permitopen_start, permitopen_len - 1);
+					spec[permitopen_len - 1] = '\0';
+					if ((split_address_port(spec, &entry->host, &portstring) == DROPBEAR_SUCCESS)
+						&& entry->host && portstring) {
+						if (strcmp(portstring, "*") == 0) {
+							valid_option = 1;
+							entry->port = PUBKEY_OPTIONS_ANY_PORT;
+							TRACE(("local port forwarding allowed to host '%s'", entry->host));
+						} else if (m_str_to_uint(portstring, &entry->port) == DROPBEAR_SUCCESS) {
+							valid_option = 1;
+							TRACE(("local port forwarding allowed to host '%s' and port '%u'",
+									entry->host, entry->port));
+						}
+					}
+
+					m_free(spec);
+					m_free(portstring);
+					break;
+				}
+			}
+
+			if (valid_option) {
+				goto next_option;
+			} else {
+				dropbear_log(LOG_WARNING, "Badly formatted permitopen= authorized_keys option");
+				goto bad_option;
+			}
+		}
 
 next_option:
 		/*
diff --git a/svr-tcpfwd.c b/svr-tcpfwd.c
index 4aa3152..7967cfa 100644
--- a/svr-tcpfwd.c
+++ b/svr-tcpfwd.c
@@ -289,6 +289,11 @@ static int newtcpdirect(struct Channel * channel) {
 		goto out;
 	}
 
+	if (!svr_pubkey_allows_local_tcpfwd(desthost, destport)) {
+		TRACE(("leave newtcpdirect: local tcp forwarding not permitted to requested destination"));
+		goto out;
+	}
+
 	snprintf(portstring, sizeof(portstring), "%u", destport);
 	channel->conn_pending = connect_remote(desthost, portstring, channel_connect_done,
 		channel, NULL, NULL, DROPBEAR_PRIO_NORMAL);