[PATCH] sshd: Add ChangeDirectory configuration directive

Étienne Buira
Sat, 13 Jun 2015 12:08:51 -0000

Directive described in sshd_config (5).

Also remove chdir warning skip code both because this can be handled
with more flexibility with this added directive and were broken since
a56086b9903b62c1c4fdedf01b68338fe4dc90e4.
---
 regress/Makefile      |  5 +++--
 regress/sftp-chdir.sh | 30 ++++++++++++++++++++++++++++++
 servconf.c            | 15 ++++++++++++++-
 servconf.h            |  2 ++
 session.c             | 23 +++++++++++++++--------
 sshd_config           |  1 +
 sshd_config.5         | 16 +++++++++++++++-
 7 files changed, 80 insertions(+), 12 deletions(-)
 create mode 100644 regress/sftp-chdir.sh

diff --git a/regress/Makefile b/regress/Makefile
index cba83f4..395241a 100644
--- a/regress/Makefile
+++ b/regress/Makefile
@@ -11,7 +11,7 @@ prep:

 clean:
 	for F in $(CLEANFILES); do rm -f $(OBJ)$$F; done
-	test -z "${SUDO}" || ${SUDO} rm -f ${SUDO_CLEAN}
+	test -z "${SUDO}" || ${SUDO} rm -df ${SUDO_CLEAN}
 	rm -rf $(OBJ).putty

 distclean:	clean
@@ -43,6 +43,7 @@ LTESTS= 	connect \
 		scp \
 		sftp \
 		sftp-chroot \
+		sftp-chdir \
 		sftp-cmds \
 		sftp-badcmds \
 		sftp-batch \
@@ -107,7 +108,7 @@ CLEANFILES=	t2.out t3.out t6.out1 t6.out2 t7.out t7.out.pub
copy.1 copy.2 \
 		key.ed25519-512.pub netcat host_krl_* host_revoked_* \
 		kh.* user_*key* agent-key.* known_hosts.* hkr.*

-SUDO_CLEAN+=	/var/run/testdata_${USER} /var/run/keycommand_${USER}
+SUDO_CLEAN+=	/var/run/testdata_${USER}
/var/run/testlanddir_${USER}{/testdata_${USER},} /var/run/keycommand_${USER}

 # Enable all malloc(3) randomisations and checks
 TEST_ENV=      "MALLOC_OPTIONS=AFGJPRX"
diff --git a/regress/sftp-chdir.sh b/regress/sftp-chdir.sh
new file mode 100644
index 0000000..46f5759
--- /dev/null
+++ b/regress/sftp-chdir.sh
@@ -0,0 +1,30 @@
+#	$OpenBSD: sftp-chroot.sh,v 1.4 2014/01/20 00:00:30 dtucker Exp $
+#	Placed in the Public Domain.
+
+tid="sftp in chroot with chdir"
+
+CHROOT=/var/run
+CHDIR=testlanddir_${USER}
+CHDIROUT=${CHROOT}/${CHDIR}
+FILENAME=testdata_${USER}
+PRIVDATA=${CHDIROUT}/${FILENAME}
+
+if [ -z "$SUDO" ]; then
+  echo "skipped: need SUDO to create file in /var/run, test won't work
without"
+  exit 0
+fi
+
+$SUDO sh -c "mkdir -p $CHDIROUT && echo mekmitastdigoat >
$PRIVDATA" || \
+	fatal "create $PRIVDATA failed"
+
+start_sshd -oChrootDirectory=$CHROOT -oForceCommand="internal-sftp"
-oChangeDirectory=$CHDIR
+
+verbose "test $tid: get"
+${SFTP} -S "$SSH" -F $OBJ/ssh_config host:${FILENAME} $COPY \
+    >>$TEST_REGRESS_LOGFILE 2>&1 || \
+	fatal "Fetch ${FILENAME} failed"
+cmp $PRIVDATA $COPY || fail "$PRIVDATA $COPY differ"
+
+$SUDO rm $PRIVDATA
+$SUDO rmdir $CHDIROUT
+
diff --git a/servconf.c b/servconf.c
index eb32db0..6812c94 100644
--- a/servconf.c
+++ b/servconf.c
@@ -167,6 +167,7 @@ initialize_server_options(ServerOptions *options)
 	options->ip_qos_bulk = -1;
 	options->version_addendum = NULL;
 	options->fingerprint_hash = -1;
+	options->change_directory = NULL;
 }

 /* Returns 1 if a string option is unset or set to "none" or 0
otherwise. */
@@ -411,7 +412,7 @@ typedef enum {
 	sAuthorizedKeysCommand, sAuthorizedKeysCommandUser,
 	sAuthenticationMethods, sHostKeyAgent, sPermitUserRC,
 	sStreamLocalBindMask, sStreamLocalBindUnlink,
-	sAllowStreamLocalForwarding, sFingerprintHash,
+	sAllowStreamLocalForwarding, sFingerprintHash, sChangeDirectory,
 	sDeprecated, sUnsupported
 } ServerOpCodes;

@@ -549,6 +550,7 @@ static struct {
 	{ "streamlocalbindunlink", sStreamLocalBindUnlink, SSHCFG_ALL },
 	{ "allowstreamlocalforwarding", sAllowStreamLocalForwarding,
SSHCFG_ALL },
 	{ "fingerprinthash", sFingerprintHash, SSHCFG_GLOBAL },
+	{ "changedirectory", sChangeDirectory, SSHCFG_ALL },
 	{ NULL, sBadOption, 0 }
 };

@@ -1826,6 +1828,15 @@ process_server_config_line(ServerOptions *options, char
*line,
 			options->fingerprint_hash = value;
 		break;

+	case sChangeDirectory:
+		arg = strdelim(&cp);
+		if (!arg || *arg == '\0')
+			fatal("%s line %d: missing directory name.",
+			    filename, linenum);
+		if (*activep && !options->change_directory)
+			options->change_directory = xstrdup(arg);
+		break;
+
 	case sDeprecated:
 		logit("%s line %d: Deprecated option %s",
 		    filename, linenum, arg);
@@ -2007,6 +2018,7 @@ copy_set_server_options(ServerOptions *dst, ServerOptions
*src, int preauth)

 	M_CP_STROPT(adm_forced_command);
 	M_CP_STROPT(chroot_directory);
+	M_CP_STROPT(change_directory);
 }

 #undef M_CP_INTOPT
@@ -2281,6 +2293,7 @@ dump_config(ServerOptions *o)
 	    o->hostbased_key_types : KEX_DEFAULT_PK_ALG);
 	dump_cfg_string(sPubkeyAcceptedKeyTypes, o->pubkey_key_types ?
 	    o->pubkey_key_types : KEX_DEFAULT_PK_ALG);
+	dump_cfg_string(sChangeDirectory, o->change_directory);

 	/* string arguments requiring a lookup */
 	dump_cfg_string(sLogLevel, log_level_name(o->log_level));
diff --git a/servconf.h b/servconf.h
index 606d80c..02089e1 100644
--- a/servconf.h
+++ b/servconf.h
@@ -194,6 +194,8 @@ typedef struct {
 	char   *auth_methods[MAX_AUTH_METHODS];

 	int	fingerprint_hash;
+
+	char   *change_directory;
 }       ServerOptions;

 /* Information about the incoming connection as used by Match */
diff --git a/session.c b/session.c
index 5a64715..6955737 100644
--- a/session.c
+++ b/session.c
@@ -1670,6 +1670,7 @@ do_child(Session *s, const char *command)
 	char *argv[ARGV_MAX];
 	const char *shell, *shell0, *hostname = NULL;
 	struct passwd *pw = s->pw;
+	char *landingdir;
 	int r = 0;

 	/* remove hostkey from the child's memory */
@@ -1784,20 +1785,26 @@ do_child(Session *s, const char *command)
 	}
 #endif

-	/* Change current directory to the user's home directory. */
-	if (chdir(pw->pw_dir) < 0) {
-		/* Suppress missing homedir warning for chroot case */
+	/* Change current directory to landing directory (either home or
+	 * set by config). */
+	if (!options.change_directory || !strcasecmp(options.change_directory,
"none"))
+		landingdir = pw->pw_dir;
+	else
+		landingdir = percent_expand(options.change_directory,
+		    "h", pw->pw_dir, "u", pw->pw_name,
(char*)NULL);
+	if (chdir(landingdir) < 0) {
 #ifdef HAVE_LOGIN_CAP
 		r = login_getcapbool(lc, "requirehome", 0);
 #endif
-		if (r || options.chroot_directory == NULL ||
-		    strcasecmp(options.chroot_directory, "none") == 0)
-			fprintf(stderr, "Could not chdir to home "
-			    "directory %s: %s\n", pw->pw_dir,
-			    strerror(errno));
+		fprintf(stderr, "Could not chdir to landing "
+		    "directory %s: %s\n", landingdir,
+		    strerror(errno));
+
 		if (r)
 			exit(1);
 	}
+	if (options.change_directory && strcasecmp(options.change_directory,
"none"))
+		free(landingdir);

 	closefrom(STDERR_FILENO + 1);

diff --git a/sshd_config b/sshd_config
index cf7d8e1..e850f0d 100644
--- a/sshd_config
+++ b/sshd_config
@@ -118,6 +118,7 @@ UsePrivilegeSeparation sandbox		# Default for new
installations.
 #PermitTunnel no
 #ChrootDirectory none
 #VersionAddendum none
+#ChangeDirectory none

 # no default banner path
 #Banner none
diff --git a/sshd_config.5 b/sshd_config.5
index 5ab4318..539c229 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -378,6 +378,17 @@ PAM or through authentication styles supported in
 .Xr login.conf 5 )
 The default is
 .Dq yes .
+.It Cm ChangeDirectory
+Specifies the pathname of a directory to
+.Xr chdir 2
+to after authentication, and after entering the chroot if
+.Cm ChrootDirectory
+were specified.
+.Pp
+The pathname may contain the following tokens that are expanded at runtime once
+the connecting user has been authenticated: %% is replaced by a literal
'%',
+%h is replaced by the home directory of the user being authenticated, and
+%u is replaced by the username of that user.
 .It Cm ChrootDirectory
 Specifies the pathname of a directory to
 .Xr chroot 2
@@ -388,7 +399,9 @@ checks that all components of the pathname are root-owned
directories
 which are not writable by any other user or group.
 After the chroot,
 .Xr sshd 8
-changes the working directory to the user's home directory.
+changes the working directory to the user's home directory (by default), or
+to directory specified with
+.Cm ChangeDirectory .
 .Pp
 The pathname may contain the following tokens that are expanded at runtime once
 the connecting user has been authenticated: %% is replaced by a literal
'%',
@@ -1041,6 +1054,7 @@ Available keywords are
 .Cm AuthorizedKeysFile ,
 .Cm AuthorizedPrincipalsFile ,
 .Cm Banner ,
+.Cm ChangeDirectory ,
 .Cm ChrootDirectory ,
 .Cm DenyGroups ,
 .Cm DenyUsers ,
--
2.0.5