ftp-server patch - restrict user to directory

Alain Williams addw at phcomp.co.uk
Mon Nov 12 10:29:53 EST 2007


Hi,

please find a patch against openssh-4.7p1

This patch:

1) Allows for an optional configuration file

2) Allows a user to be restricted to a directory and it's children.

Enjoy

-- 
Alain Williams
Linux Consultant - Mail systems, Web sites, Networking, Programmer, IT Lecturer.
+44 (0) 787 668 0256  http://www.phcomp.co.uk/
Parliament Hill Computers Ltd. Registration Information: http://www.phcomp.co.uk/contact.php
Chairman of UKUUG: http://www.ukuug.org/
#include <std_disclaimer.h>
-------------- next part --------------
--- sftp-server.c.orig	2007-05-20 06:09:05.000000000 +0100
+++ sftp-server.c	2007-11-11 23:25:29.000000000 +0000
@@ -31,7 +31,6 @@
 #include <stdlib.h>
 #include <stdio.h>
 #include <string.h>
-#include <pwd.h>
 #include <time.h>
 #include <unistd.h>
 #include <stdarg.h>
@@ -44,6 +43,7 @@
 
 #include "sftp.h"
 #include "sftp-common.h"
+#include "pathnames.h"
 
 /* helper */
 #define get_int64()			buffer_get_int64(&iqueue);
@@ -74,6 +74,141 @@
 	Attrib attrib;
 };
 
+/* Name of the server configuration file. */
+char *config_file_name = _PATH_SFTP_CONFIG_FILE;
+
+/* If not NULL restrict the user to under this directory */
+char* RestrictDirectory;
+
+/* **** Start parsing config file code **** */
+
+/* Read the optional config file. If mandatory the file must exist.
+ * Exit on error.
+ */
+static void
+load_sftp_config(char* file_name, int mandatory)
+{
+	char line[1024], *cp, *arg, *opt;
+	FILE *f;
+	int lineno = 0;
+
+	debug2("%s: filename %s mandatory %d", __func__, file_name, mandatory);
+	if ((f = fopen(file_name, "r")) == NULL) {
+		if( ! mandatory)	/* config file may be optional */
+			return;
+
+		perror(file_name);
+		exit(1);
+	}
+
+	/* Read a line at a time */
+	while(fgets(line, sizeof(line), f)) {
+		lineno++;
+		cp = line + strspn(line, " \t");
+		if(*cp == '#' || *cp == '\0' || *cp == '\n')	/* Ignore comments and empty lines */
+			continue;
+		if( ! (arg = strchr(cp, '\n')))
+			fatal("Line %d in file %.100s too long", lineno, file_name);
+		*arg = '\0';
+
+		opt = strdelim(&cp);	/* Get directive */
+		debug3("opt='%s' rest='%s'\n", opt, cp);
+
+		/* Find what argument we have */
+		if( ! strcmp(opt, "RestrictDirectory")) {
+			RestrictDirectory = xstrdup(strdelim(&cp));	/* Get pathname */
+			if(cp)
+				fatal("Line %d in file %.100s: too many arguments to %s", lineno, file_name, opt);
+		} else
+			fatal("Line %d in file %.100s: unknown directive: %.100s", lineno, file_name, opt);
+	}
+
+	/* All done */
+	fclose(f);
+	debug2("%s: done config, lines %d", __func__, lineno);
+}
+
+/* **** End parsing config file code **** */
+
+/* Set up options once we know who the user is.
+ * Look at pw for details.
+ */
+static void
+set_user_opts(void)
+{
+	/* Does the restict directory start "~/xxx" or have the value "~" ?
+	 * If so relace with the user's home directory.
+	 * Don't attempt to expand arbitrary "~user/".
+	 * If a directory has a trailing "/" then listings of that directory will not be allowed.
+	 */
+	if(RestrictDirectory && RestrictDirectory[0] == '~' &&
+	    (RestrictDirectory[1] == '/' || RestrictDirectory[1] == '\0')) {
+		char* tmp = xmalloc(strlen(RestrictDirectory) + strlen(pw->pw_dir));
+		strcat(strcpy(tmp, pw->pw_dir), RestrictDirectory + 1);
+		free(RestrictDirectory);
+		RestrictDirectory = tmp;
+	}
+
+	/* It is possible, if unlikely, that the restricted directory will have been specified with
+	 * a symlink or .. in it. That will totally blow comparisions in allowed_access(). Resolve this.
+	 */
+	if(RestrictDirectory) {
+		char resolvedname[MAXPATHLEN];
+		char* tmp;
+
+		if( ! realpath(RestrictDirectory, resolvedname))
+			fatal("Can't get realpath on %.100s as: %s", RestrictDirectory, strerror(errno));
+
+		tmp = xstrdup(resolvedname);
+		free(RestrictDirectory);
+		RestrictDirectory = tmp;
+	}
+}
+
+/* Check that the user is allowed to access the path for the purpose reason.
+ * This implements the RestrictDirectory option.
+ * Return true if allowed.
+ * Log failed access attempts.
+ */
+static int
+allowed_access(char* path, char* reason)
+{
+	char resolvedname[MAXPATHLEN];
+	int restlen;
+
+	if( ! RestrictDirectory)	/* No restriction, allow */
+		return(1);
+
+	/* If we can't convert the name - deem it an error */
+	if( ! realpath(path, resolvedname)) {
+		error("Can't get realpath on %.100s as: %s", path, strerror(errno));
+		return(0);
+	}
+
+	restlen = strlen(RestrictDirectory);
+
+	/* Allow the directory itself.
+	 * Trap where the canny user with a '~/' restriction tries to list '/home/joe/' - ie adds the '/'.
+	 */
+	if( ! strcmp(resolvedname, RestrictDirectory) && RestrictDirectory[restlen - 1] != '/')
+		return(1);
+
+	/* Find length before '/' of restricting directory */
+	if(RestrictDirectory[restlen - 1] == '/')
+		restlen--;
+
+	/* If the first bit matches it is acceptable.
+	 * Check for a '/' else ~fred will allow access to ~freddy.
+	 */
+	if( ! strncmp(resolvedname, RestrictDirectory, restlen) &&
+	    resolvedname[restlen] == '/')
+		return(1);
+
+	error("Restricted access, %s disallowed for %.100s", reason, path);
+
+	return(0);
+}
+
 static int
 errno_to_portable(int unixerrno)
 {
@@ -813,20 +948,24 @@
 	id = get_int();
 	path = get_string(NULL);
 	debug3("request %u: opendir", id);
-	logit("opendir \"%s\"", path);
-	dirp = opendir(path);
-	if (dirp == NULL) {
-		status = errno_to_portable(errno);
-	} else {
-		handle = handle_new(HANDLE_DIR, path, 0, dirp);
-		if (handle < 0) {
-			closedir(dirp);
+
+	if(allowed_access(path, "opendir")) {	/* RestrictDirectory ? */
+		logit("opendir \"%s\"", path);
+		dirp = opendir(path);
+		if (dirp == NULL) {
+			status = errno_to_portable(errno);
 		} else {
-			send_handle(id, handle);
-			status = SSH2_FX_OK;
+			handle = handle_new(HANDLE_DIR, path, 0, dirp);
+			if (handle < 0) {
+				closedir(dirp);
+			} else {
+				send_handle(id, handle);
+				status = SSH2_FX_OK;
+			}
 		}
+	} else
+		status = errno_to_portable(EPERM);
 
-	}
 	if (status != SSH2_FX_OK)
 		send_status(id, status);
 	xfree(path);
@@ -899,9 +1038,14 @@
 	id = get_int();
 	name = get_string(NULL);
 	debug3("request %u: remove", id);
-	logit("remove name \"%s\"", name);
-	ret = unlink(name);
-	status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+
+	if(allowed_access(name, "remove")) { /* RestrictDirectory ? */
+		logit("remove name \"%s\"", name);
+		ret = unlink(name);
+		status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+	} else
+		status = errno_to_portable(EPERM);
+
 	send_status(id, status);
 	xfree(name);
 }
@@ -920,9 +1064,13 @@
 	mode = (a->flags & SSH2_FILEXFER_ATTR_PERMISSIONS) ?
 	    a->perm & 0777 : 0777;
 	debug3("request %u: mkdir", id);
-	logit("mkdir name \"%s\" mode 0%o", name, mode);
-	ret = mkdir(name, mode);
-	status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+	if(allowed_access(name, "mkdir")) { /* RestrictDirectory ? */
+		logit("mkdir name \"%s\" mode 0%o", name, mode);
+		ret = mkdir(name, mode);
+		status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+	} else
+		status = errno_to_portable(EPERM);
+
 	send_status(id, status);
 	xfree(name);
 }
@@ -937,9 +1085,13 @@
 	id = get_int();
 	name = get_string(NULL);
 	debug3("request %u: rmdir", id);
-	logit("rmdir name \"%s\"", name);
-	ret = rmdir(name);
-	status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+	if(allowed_access(name, "rmdir")) { /* RestrictDirectory ? */
+		logit("rmdir name \"%s\"", name);
+		ret = rmdir(name);
+		status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+	} else
+		status = errno_to_portable(EPERM);
+
 	send_status(id, status);
 	xfree(name);
 }
@@ -950,6 +1102,7 @@
 	char resolvedname[MAXPATHLEN];
 	u_int32_t id;
 	char *path;
+	static int num_realpaths = 0;
 
 	id = get_int();
 	path = get_string(NULL);
@@ -958,15 +1111,24 @@
 		path = xstrdup(".");
 	}
 	debug3("request %u: realpath", id);
-	verbose("realpath \"%s\"", path);
-	if (realpath(path, resolvedname) == NULL) {
-		send_status(id, errno_to_portable(errno));
-	} else {
-		Stat s;
-		attrib_clear(&s.attrib);
-		s.name = s.long_name = resolvedname;
-		send_names(id, 1, &s);
-	}
+
+	/* RestrictDirectory ? - could be used to probe the file system.
+	 * Need to allow on realpath since that is done automatically to get the $HOME.
+	 * The user must then cd <whereever> before s/he can do anything else.
+	 */
+	if(num_realpaths++ == 0 || allowed_access(path, "realpath")) {
+		verbose("realpath \"%s\"", path);
+		if (realpath(path, resolvedname) == NULL) {
+			send_status(id, errno_to_portable(errno));
+		} else {
+			Stat s;
+			attrib_clear(&s.attrib);
+			s.name = s.long_name = resolvedname;
+			send_names(id, 1, &s);
+		}
+	} else
+		send_status(id, errno_to_portable(EPERM));
+
 	xfree(path);
 }
 
@@ -982,6 +1144,14 @@
 	oldpath = get_string(NULL);
 	newpath = get_string(NULL);
 	debug3("request %u: rename", id);
+
+	if( ! allowed_access(oldpath, "rename") || ! allowed_access(newpath, "rename")) { /* RestrictDirectory ? */
+		send_status(id, errno_to_portable(EPERM));
+		xfree(oldpath);
+		xfree(newpath);
+		return;
+	}
+
 	logit("rename old \"%s\" new \"%s\"", oldpath, newpath);
 	status = SSH2_FX_FAILURE;
 	if (lstat(oldpath, &sb) == -1)
@@ -1038,17 +1208,22 @@
 	id = get_int();
 	path = get_string(NULL);
 	debug3("request %u: readlink", id);
-	verbose("readlink \"%s\"", path);
-	if ((len = readlink(path, buf, sizeof(buf) - 1)) == -1)
-		send_status(id, errno_to_portable(errno));
-	else {
-		Stat s;
-
-		buf[len] = '\0';
-		attrib_clear(&s.attrib);
-		s.name = s.long_name = buf;
-		send_names(id, 1, &s);
-	}
+
+	if(allowed_access(path, "readlink")) { /* RestrictDirectory ? */
+		verbose("readlink \"%s\"", path);
+		if ((len = readlink(path, buf, sizeof(buf) - 1)) == -1)
+			send_status(id, errno_to_portable(errno));
+		else {
+			Stat s;
+
+			buf[len] = '\0';
+			attrib_clear(&s.attrib);
+			s.name = s.long_name = buf;
+			send_names(id, 1, &s);
+		}
+	} else
+		send_status(id, errno_to_portable(EPERM));
+
 	xfree(path);
 }
 
@@ -1063,10 +1238,15 @@
 	oldpath = get_string(NULL);
 	newpath = get_string(NULL);
 	debug3("request %u: symlink", id);
-	logit("symlink old \"%s\" new \"%s\"", oldpath, newpath);
-	/* this will fail if 'newpath' exists */
-	ret = symlink(oldpath, newpath);
-	status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+
+	if( ! allowed_access(oldpath, "symlink") || ! allowed_access(newpath, "symlink")) { /* RestrictDirectory ? */
+		logit("symlink old \"%s\" new \"%s\"", oldpath, newpath);
+		/* this will fail if 'newpath' exists */
+		ret = symlink(oldpath, newpath);
+		status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+	} else
+		status = errno_to_portable(EPERM);
+
 	send_status(id, status);
 	xfree(oldpath);
 	xfree(newpath);
@@ -1203,7 +1383,7 @@
 	extern char *__progname;
 
 	fprintf(stderr,
-	    "usage: %s [-he] [-l log_level] [-f log_facility]\n", __progname);
+	    "usage: %s [-he] [-l log_level] [-F config_file] [-f log_facility]\n", __progname);
 	exit(1);
 }
 
@@ -1215,6 +1395,7 @@
 	ssize_t len, olen, set_size;
 	SyslogFacility log_facility = SYSLOG_FACILITY_AUTH;
 	char *cp, buf[4*4096];
+	int config_file_mandatory = 0;
 
 	extern char *optarg;
 	extern char *__progname;
@@ -1225,7 +1406,7 @@
 	__progname = ssh_get_progname(argv[0]);
 	log_init(__progname, log_level, log_facility, log_stderr);
 
-	while (!skipargs && (ch = getopt(argc, argv, "C:f:l:che")) != -1) {
+	while (!skipargs && (ch = getopt(argc, argv, "C:f:F:l:che")) != -1) {
 		switch (ch) {
 		case 'c':
 			/*
@@ -1247,12 +1428,18 @@
 			if (log_level == SYSLOG_FACILITY_NOT_SET)
 				error("Invalid log facility \"%s\"", optarg);
 			break;
+		case 'F':
+			config_file_name = optarg;
+			config_file_mandatory = 1;	/* Since it is specified, it is a must */
+			break;
 		case 'h':
 		default:
 			usage();
 		}
 	}
 
+	load_sftp_config(config_file_name, config_file_mandatory);
+
 	log_init(__progname, log_level, log_facility, log_stderr);
 
 	if ((cp = getenv("SSH_CONNECTION")) != NULL) {
@@ -1271,6 +1458,8 @@
 	logit("session opened for local user %s from [%s]",
 	    pw->pw_name, client_addr);
 
+	set_user_opts();
+
 	handle_init();
 
 	in = dup(STDIN_FILENO);
--- sftp-server.8.orig	2007-06-05 09:27:13.000000000 +0100
+++ sftp-server.8	2007-11-11 23:15:51.000000000 +0000
@@ -60,6 +60,12 @@
 The possible values are: DAEMON, USER, AUTH, LOCAL0, LOCAL1, LOCAL2,
 LOCAL3, LOCAL4, LOCAL5, LOCAL6, LOCAL7.
 The default is AUTH.
+.It Fl F Ar configfile
+Specifies a configuration file that shall be used, if this file is not found
+a fatal error is generated.
+The defualt file is
+.Xr /etc/ssh/sftp_config
+if this file is not found no error is generated.
 .It Fl l Ar log_level
 Specifies which messages will be logged by
 .Nm .
@@ -72,6 +78,27 @@
 DEBUG2 and DEBUG3 each specify higher levels of debugging output.
 The default is ERROR.
 .El
+.Sh CONFIGURATION FILE FORMAT
+The file contains keyword-argument pairs, one per line.
+Lines starting with
+.Ql #
+and empty lines are interpreted as comments.
+Arguments may optionally be enclosed in double quotes
+.Pq \&"
+in order to represent arguments containing spaces.
+.Bl -tag -width Ds
+.It Cm RestrictDirectory
+Restrict the user's activities to the argument directory and the directories below it.
+If the directory is
+.Ql ~
+the user's home directory is used.
+Expansion of arbitrary
+.Ql ~user/
+is not done.
+If a directory has a trailing
+.Ql /
+then listings of that directory will not be allowed.
+.El
 .Sh SEE ALSO
 .Xr sftp 1 ,
 .Xr ssh 1 ,


More information about the openssh-unix-dev mailing list