wolfsshd: apply ForceCommand to SFTP and SCP

A ForceCommand other than a bare "internal-sftp" now denies the SFTP
subsystem, and any ForceCommand denies SCP, as sshd_config(5) describes
it: a forced command applies to shell, command, and subsystem execution.
The session dispatch checks the configured command alongside a
certificate's, so either one on its own confines the session.

- the subsystem arm and both SCP arms consult the configured
  ForceCommand, not only a certificate's force-command
- "internal-sftp" followed by options denies SFTP, since no option is
  applied, while the shell still refuses it as "internal-sftp"
- every site testing for "internal-sftp" now shares one predicate
- root's transfer refusal no longer points at ForceCommand internal-sftp,
  which does not permit SCP
- tests require a ForceCommand to deny SFTP and SCP, read the daemon's
  decisions from its log, and pair a configured internal-sftp with a cert

Issue: ZD-22623
Issue: F-10597
pull/1356/head
John Safranek 2026-10-15 14:05:27 -07:00 committed by philljj
parent d04bad3a6e
commit 0f4002bc4e
3 changed files with 229 additions and 36 deletions

View File

@ -21,7 +21,45 @@ source ./start_sshd.sh
# root daemon holding the shared test port, so every later test in the suite
# would talk to this config. stop_wolfsshd clears PID, so this is a no-op after
# each explicit stop below.
trap stop_wolfsshd EXIT
cleanup() {
stop_wolfsshd
rm -f "$TESTDIR/scp_src.dat" "$TESTDIR/scp_dst.dat"
rm -f "$TESTDIR/sshd_config_test_forcedcmd_args"
rm -f "$TESTDIR/sshd_config_test_forcedcmd_prefix"
}
trap cleanup EXIT
# The daemon's decisions are read from log.txt, which the whole run shares:
# log_mark notes its length and log_since prints what came after.
log_mark() {
sudo cat "$TESTDIR/log.txt" | wc -l
}
log_since() {
sudo cat "$TESTDIR/log.txt" | tail -n +`expr $1 + 1`
}
# Native SCP is an exec, a path of its own. wolfscp masks its exit code, so
# the decision is read from the log. Fails the test unless SCP is denied.
check_scp_denied() {
if [ ! -x "$TEST_SCP" ]; then
return 0
fi
MARK=`log_mark`
rm -f "$TESTDIR/scp_dst.dat"
echo "scp payload" > "$TESTDIR/scp_src.dat"
$TEST_SCP -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY \
-S"$TESTDIR/scp_src.dat:$TESTDIR/scp_dst.dat" \
-H $TEST_HOST -p $TEST_PORT > /dev/null 2>&1 || true
if ! log_since $MARK | grep -q "denying SCP"; then
echo "SCP should be denied with $1"
exit 1
fi
if [ -f "$TESTDIR/scp_dst.dat" ]; then
echo "SCP wrote its destination despite $1"
exit 1
fi
}
cat <<EOF > sshd_config_test_forcedcmd
Port $TEST_PORT
@ -44,6 +82,7 @@ cd ../../..
TEST_CLIENT="./examples/client/client"
TEST_SFTP="./examples/sftpclient/wolfsftp"
TEST_SCP="./examples/scpclient/wolfscp"
PRIVATE_KEY="./keys/hansel-key-ecc.der"
PUBLIC_KEY="./keys/hansel-key-ecc.pub"
@ -59,12 +98,91 @@ fi
set -e
echo exit | $TEST_SFTP -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -h $TEST_HOST -p $TEST_PORT
check_scp_denied "ForceCommand internal-sftp"
cd "$TESTDIR"
stop_wolfsshd
# A configured ForceCommand that is not "internal-sftp" must still permit the
# SFTP subsystem. Only a certificate force-command denies file transfer, so a
# long-standing ForceCommand config keeps working.
# sshd_config(5) permits options after "internal-sftp", but none is
# applied: shell is still refused and SFTP is refused too.
cat <<EOF > sshd_config_test_forcedcmd_args
Port $TEST_PORT
Protocol 2
LoginGraceTime 600
PermitRootLogin yes
PasswordAuthentication yes
PermitEmptyPasswords no
UsePrivilegeSeparation no
UseDNS no
HostKey $TESTDIR/../../../keys/server-key.pem
AuthorizedKeysFile $TESTDIR/authorized_keys_test
Match User $USER
ForceCommand internal-sftp -u 0002
EOF
start_wolfsshd "sshd_config_test_forcedcmd_args"
cd ../../..
# The refused login exits non-zero and this runs under the "set -e" above.
# Running "internal-sftp -u 0002" as a command would print no "bob" either,
# so the refusal is read from the log.
MARK=`log_mark`
RESULT=$( $TEST_CLIENT -c 'echo bob' -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -h $TEST_HOST -p $TEST_PORT ) || true
if echo $RESULT | grep bob ||
! log_since $MARK | grep -q "Only SFTP connections allowed"; then
echo "Shell login should fail with internal-sftp and options"
exit 1
fi
MARK=`log_mark`
if echo exit | $TEST_SFTP -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY \
-h $TEST_HOST -p $TEST_PORT; then
echo "SFTP should fail with internal-sftp and options"
exit 1
fi
if ! log_since $MARK | grep -q "internal-sftp options are not supported"; then
echo "SFTP should be refused for its internal-sftp options"
exit 1
fi
cd "$TESTDIR"
stop_wolfsshd
# A name that only starts with "internal-sftp" is some other command.
sed 's/ForceCommand internal-sftp -u 0002/ForceCommand internal-sftpd/' \
sshd_config_test_forcedcmd_args > sshd_config_test_forcedcmd_prefix
start_wolfsshd "sshd_config_test_forcedcmd_prefix"
cd ../../..
# Refused as a forced command, not as internal-sftp with an option "d".
MARK=`log_mark`
if echo exit | $TEST_SFTP -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY \
-h $TEST_HOST -p $TEST_PORT; then
echo "SFTP should fail with a command named like internal-sftp"
exit 1
fi
if ! log_since $MARK | grep -q "denying subsystem request" ||
log_since $MARK | grep -q "options are not supported"; then
echo "internal-sftpd was taken for internal-sftp"
exit 1
fi
# The shell runs it as the forced command, without the internal-sftp refusal.
MARK=`log_mark`
$TEST_CLIENT -c 'echo bob' -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY \
-h $TEST_HOST -p $TEST_PORT > /dev/null 2>&1 || true
if log_since $MARK | grep -q "Only SFTP connections allowed"; then
echo "Shell refused internal-sftpd as internal-sftp"
exit 1
fi
cd "$TESTDIR"
stop_wolfsshd
# A ForceCommand other than "internal-sftp" denies the SFTP subsystem and
# native SCP: the client may only run the forced command.
cat <<EOF > sshd_config_test_forcedcmd_sftp
Port $TEST_PORT
Protocol 2
@ -84,7 +202,13 @@ EOF
start_wolfsshd "sshd_config_test_forcedcmd_sftp"
cd ../../..
echo exit | $TEST_SFTP -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -h $TEST_HOST -p $TEST_PORT
if echo exit | $TEST_SFTP -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY \
-h $TEST_HOST -p $TEST_PORT; then
echo "SFTP should fail with a forced command other than internal-sftp"
exit 1
fi
check_scp_denied "a forced command"
cd "$TESTDIR"
stop_wolfsshd

View File

@ -228,8 +228,8 @@ HostKey $HOSTKEY
TrustedUserCAKeys $WORK/trusted-cas.pub
EOF
# Second config, adding a ForceCommand that is not "internal-sftp". Used to
# separate the config-sourced command from the certificate's.
# Second config, adding a ForceCommand that is not "internal-sftp", which
# denies SFTP on its own.
cat > "$WORK/sshd_config_ossh_fc" <<EOF
Port $PORT
Protocol 2
@ -243,6 +243,22 @@ Match User $LOGINUSER
ForceCommand /bin/echo
EOF
# Third config, a ForceCommand "internal-sftp", which permits SFTP. Only here
# does a certificate's force-command change the outcome, so it shows the
# configured command does not mask the certificate's.
cat > "$WORK/sshd_config_ossh_isftp" <<EOF
Port $PORT
Protocol 2
UsePrivilegeSeparation no
UseDNS no
PasswordAuthentication no
HostKey $HOSTKEY
TrustedUserCAKeys $WORK/trusted-cas.pub
Match User $LOGINUSER
ForceCommand internal-sftp
EOF
CONFIG="$WORK/sshd_config_ossh"
FAIL=0
DRIVER=""
@ -453,13 +469,16 @@ run_suite() { # driver
sftp_check "internal-sftp sftp" "$ED" \
"$CERTS/$LOGINUSER-ossh-internalsftp-cert.pub" 0
# A configured ForceCommand is not a certificate force-command: on its
# own it must not deny SFTP, and it must not mask one carried by a
# certificate.
# A configured ForceCommand denies SFTP on its own.
CONFIG="$WORK/sshd_config_ossh_fc"
sftp_check "config forcecmd sftp" "$ED" \
"$CERTS/$LOGINUSER-ossh-cert.pub" 1
# A configured "internal-sftp" does not mask a certificate's.
CONFIG="$WORK/sshd_config_ossh_isftp"
sftp_check "config isftp sftp" "$ED" \
"$CERTS/$LOGINUSER-ossh-cert.pub" 0
sftp_check "config+cert sftp deny" "$ED" \
sftp_check "config isftp+cert sftp deny" "$ED" \
"$CERTS/$LOGINUSER-ossh-forcecmd-cert.pub" 1
CONFIG="$WORK/sshd_config_ossh"
else
@ -476,6 +495,12 @@ run_suite() { # driver
"$CERTS/$LOGINUSER-ossh-forcecmd-cert.pub" 1
scp_check "internal-sftp scp" "$ED" \
"$CERTS/$LOGINUSER-ossh-internalsftp-cert.pub" 1
# A configured ForceCommand denies it too.
CONFIG="$WORK/sshd_config_ossh_fc"
scp_check "config forcecmd scp" "$ED" \
"$CERTS/$LOGINUSER-ossh-cert.pub" 1
CONFIG="$WORK/sshd_config_ossh"
fi
}

View File

@ -1734,6 +1734,47 @@ static const char* GetCertForcedCmd(WOLFSSHD_CONNECTION* conn)
#endif
}
static const char internalSftp[] = "internal-sftp";
#define INTERNAL_SFTP_SZ ((word32)(sizeof(internalSftp) - 1))
/* 1 when 'forcedCmd' is "internal-sftp". Only the first token is matched;
* sshd_config(5) permits options after it. */
static int ForcedCmdIsInternalSftp(const char* forcedCmd)
{
return forcedCmd != NULL &&
WSTRNCMP(forcedCmd, internalSftp, INTERNAL_SFTP_SZ) == 0 &&
(forcedCmd[INTERNAL_SFTP_SZ] == '\0' ||
WISSPACE((unsigned char)forcedCmd[INTERNAL_SFTP_SZ]));
}
/* 1 when 'forcedCmd' is set to anything but a bare "internal-sftp". No
* internal-sftp option is applied, so one given denies SFTP rather than
* being ignored. NULL forbids nothing. */
static int ForcedCmdDeniesSftp(const char* forcedCmd)
{
const char* opt;
if (forcedCmd == NULL) {
return 0;
}
if (!ForcedCmdIsInternalSftp(forcedCmd)) {
return 1;
}
for (opt = forcedCmd + INTERNAL_SFTP_SZ; *opt != '\0'; opt++) {
if (!WISSPACE((unsigned char)*opt)) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] internal-sftp options are not supported: %s",
forcedCmd);
return 1;
}
}
return 0;
}
#ifdef WOLFSSH_SCP
static int SCP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
WPASSWD* pPasswd, WOLFSSHD_CONFIG* usrConf)
@ -2147,7 +2188,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
/* @TODO check for conpty support LoadLibrary()and GetProcAddress(). */
if (forcedCmd != NULL && WSTRCMP(forcedCmd, "internal-sftp") == 0) {
if (ForcedCmdIsInternalSftp(forcedCmd)) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Only SFTP connections allowed for user "
"%s", wolfSSH_GetUsername(ssh));
@ -2818,7 +2859,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh,
forcedCmd = subCmd;
}
if (forcedCmd != NULL && WSTRCMP(forcedCmd, "internal-sftp") == 0) {
if (ForcedCmdIsInternalSftp(forcedCmd)) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Only SFTP connections allowed for user "
"%s", wolfSSH_GetUsername(ssh));
@ -3755,10 +3796,10 @@ static int UserAuthResult(byte result,
}
#if defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP)
/* Returns 1 if root's forced-commands-only ForceCommand isn't
* "internal-sftp": SFTP/SCP never honor ForceCommand (only SHELL_Subsystem
* does), so this blocks the confinement bypass. No-op on Windows
* (pPasswd is NULL there). */
/* 1 when root is under forced-commands-only and its ForceCommand isn't a
* bare "internal-sftp". The dispatch denies SCP under any ForceCommand
* before this, so root never gets SCP there. No-op on Windows (pPasswd is
* NULL there). */
static int IsRootTransferBlocked(WPASSWD* pPasswd, WOLFSSHD_CONFIG* usrConf)
{
#ifdef _WIN32
@ -3777,7 +3818,7 @@ static int IsRootTransferBlocked(WPASSWD* pPasswd, WOLFSSHD_CONFIG* usrConf)
}
forcedCmd = wolfSSHD_ConfigGetForcedCmd(usrConf);
return (forcedCmd == NULL || WSTRCMP(forcedCmd, "internal-sftp") != 0);
return forcedCmd == NULL || ForcedCmdDeniesSftp(forcedCmd);
#endif
}
@ -3787,8 +3828,8 @@ static int RejectRootTransfer(WPASSWD* pPasswd, WOLFSSHD_CONFIG* usrConf,
const char* what)
{
if (IsRootTransferBlocked(pPasswd, usrConf)) {
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] %s for root requires ForceCommand "
"internal-sftp under forced-commands-only", what);
wolfSSH_Log(WS_LOG_ERROR, "[SSHD] %s for root is not allowed "
"under forced-commands-only", what);
return 1;
}
return 0;
@ -3929,7 +3970,8 @@ static void* HandleConnection(void* arg)
WPASSWD* pPasswd = NULL;
WOLFSSHD_CONFIG* usrConf;
char* usr;
const char* forcedCmd = NULL;
const char* certCmd = NULL;
const char* confCmd = NULL;
/* get configuration for user */
usr = wolfSSH_GetUsername(ssh);
@ -3971,14 +4013,15 @@ static void* HandleConnection(void* arg)
break;
case WOLFSSH_SESSION_SUBSYSTEM:
/* A certificate force-command overrides the requested
* subsystem. "internal-sftp" still permits SFTP; any other
* command denies file transfer (fail closed). */
forcedCmd = GetCertForcedCmd(conn);
if (forcedCmd != NULL &&
WSTRCMP(forcedCmd, "internal-sftp") != 0) {
/* A force-command, configured or from a certificate,
* overrides the requested subsystem. Only
* "internal-sftp" still permits SFTP. */
certCmd = GetCertForcedCmd(conn);
confCmd = wolfSSHD_ConfigGetForcedCmd(usrConf);
if (ForcedCmdDeniesSftp(certCmd) ||
ForcedCmdDeniesSftp(confCmd)) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Cert force-command set for user %s; "
"[SSHD] Force-command set for user %s; "
"denying subsystem request",
wolfSSH_GetUsername(ssh));
ret = WS_FATAL_ERROR;
@ -4005,11 +4048,11 @@ static void* HandleConnection(void* arg)
case WS_SCP_INIT:
#ifdef WOLFSSH_SCP
/* any certificate force-command, including
* internal-sftp, denies SCP */
if (forcedCmd != NULL) {
/* any force-command, including internal-sftp,
* denies SCP */
if (certCmd != NULL || confCmd != NULL) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Cert force-command set for user "
"[SSHD] Force-command set for user "
"%s; denying SCP",
wolfSSH_GetUsername(ssh));
ret = WS_FATAL_ERROR;
@ -4054,11 +4097,12 @@ static void* HandleConnection(void* arg)
/* SCP can be an exec type */
if (ret == WS_SCP_INIT) {
#ifdef WOLFSSH_SCP
/* any certificate force-command denies SCP */
forcedCmd = GetCertForcedCmd(conn);
if (forcedCmd != NULL) {
/* any force-command denies SCP */
certCmd = GetCertForcedCmd(conn);
confCmd = wolfSSHD_ConfigGetForcedCmd(usrConf);
if (certCmd != NULL || confCmd != NULL) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Cert force-command set for user %s; "
"[SSHD] Force-command set for user %s; "
"denying SCP", wolfSSH_GetUsername(ssh));
ret = WS_FATAL_ERROR;
}