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