Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/wolfsshd/test/run_all_sshd_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ test_cases=(
"sshd_large_sftp_test.sh"
"sshd_bad_sftp_test.sh"
"sshd_sftp_idle_cpu_test.sh"
"sshd_bad_subsystem_test.sh"
"sshd_scp_fail.sh"
"sshd_term_close_test.sh"
"sshd_stdin_eof_test.sh"
Expand Down
68 changes: 68 additions & 0 deletions apps/wolfsshd/test/sshd_bad_subsystem_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/bin/sh

# sshd local test: a subsystem the daemon does not serve is refused at the
# request, so the client sees CHANNEL_FAILURE rather than a session that
# is accepted and then dropped. Uses the system OpenSSH client, since the
# in-tree clients only ask for sftp.

# Not named PWD: the shell rewrites that variable on every cd, so a saved
# copy would not survive the cd to the repository root below.
TESTDIR=`pwd`
cd ../../..

USER=`whoami`
PRIVATE_KEY="./keys/hansel-key-ecc.pem"

if [ -z "$1" ] || [ -z "$2" ]; then
echo "expecting host and port as arguments"
echo "./sshd_bad_subsystem_test.sh 127.0.0.1 22222"
exit 1
fi

if ! command -v ssh >/dev/null 2>&1; then
echo "OpenSSH client not found, skipping"
exit 77
fi

# OpenSSH refuses a key file other users can read.
KEY=`mktemp`
cat "$PRIVATE_KEY" > "$KEY"
chmod 600 "$KEY"
OUT=`mktemp`

ssh_to_sshd() {
ssh -p "$2" -i "$KEY" -o IdentitiesOnly=yes -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o PreferredAuthentications=publickey \
-o BatchMode=yes -o ConnectTimeout=5 "$USER@$1" "$3" "$4"
}

# Control: the same client and key can run a command.
ssh_to_sshd "$1" "$2" "echo ok" > "$OUT" 2>&1
RESULT=$?
if [ "$RESULT" != "0" ] || ! grep -q "^ok" "$OUT"; then
echo "Control exec through OpenSSH failed ($RESULT):"
cat "$OUT"
rm -f "$KEY" "$OUT"
exit 1
fi

# A subsystem nothing serves: the client reports the refusal and exits
# non-zero.
ssh_to_sshd "$1" "$2" -s no-such-subsystem > "$OUT" 2>&1
RESULT=$?
if [ "$RESULT" = "0" ]; then
echo "Expecting the unknown subsystem request to fail"
cat "$OUT"
rm -f "$KEY" "$OUT"
exit 1
fi
if ! grep -q "subsystem request failed" "$OUT"; then
echo "Expecting the client to report the refused subsystem request:"
cat "$OUT"
rm -f "$KEY" "$OUT"
exit 1
fi

rm -f "$KEY" "$OUT"
cd "$TESTDIR"
exit 0
90 changes: 90 additions & 0 deletions apps/wolfsshd/wolfsshd.c
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,92 @@ static void CleanupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx,
(void)conf;
}

/* Answers a shell, exec or subsystem request as it arrives: a session this
* build cannot serve is refused with CHANNEL_FAILURE, rather than accepted
* and then dropped once the session is up. Returns 0 to accept and 1 to
* refuse. The command is NULL when the request carried none that fit. */
static int SessionRequestCb(WOLFSSH_CHANNEL* channel, void* vCtx)
{
WOLFSSHD_CONNECTION* conn = (WOLFSSHD_CONNECTION*)vCtx;
const char* cmd;
const char* reason = NULL;
int rej = 1;

if (conn == NULL || channel == NULL) {
return 1;
}

cmd = wolfSSH_ChannelGetSessionCommand(channel);
switch (wolfSSH_ChannelGetSessionType(channel)) {
case WOLFSSH_SESSION_SHELL:
#ifdef WOLFSSH_SHELL
rej = 0;
#else
reason = "shell support is disabled";
#endif
break;

case WOLFSSH_SESSION_EXEC:
if (cmd == NULL) {
reason = "exec request carried no command";
break;
}
#ifdef WOLFSSH_SCP
if (WSTRNCMP(cmd, "scp", 3) == 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCP prefix is granted as a valid exec request · Logic errors

SessionRequestCb() grants every exec command starting with scp, so an SCP-only daemon accepts names such as scpbackup and routes them into SCP handling. Known #11664 is the downstream classifier; this is the new daemon policy gate.

Suggested fix: Require a length-aware scp token boundary before granting, and reject embedded-NUL or otherwise unparseable SCP commands.
Basis: OpenBSD scp(1) SYNOPSIS defines scp as the command name followed by options and operands, not as a prefix of another command name.

rej = 0;
break;
}
#endif
#ifdef WOLFSSH_SHELL
rej = 0;
#else
reason = "exec support is disabled";
#endif
break;

case WOLFSSH_SESSION_SUBSYSTEM:
if (cmd == NULL) {
reason = "subsystem request carried no name";
}
#ifdef WOLFSSH_SFTP
/* Matched whole, length and bytes, as the sftp divert asks:
* sftp with an embedded NUL is another subsystem. */
else if (wolfSSH_ChannelGetSessionCommandSz(channel)
== (word32)WSTRLEN("sftp")
&& WSTRCMP(cmd, "sftp") == 0) {
rej = 0;
}
#endif
else {
reason = "unknown or unsupported subsystem";
}
break;

case WOLFSSH_SESSION_UNKNOWN:
case WOLFSSH_SESSION_TERMINAL:
default:
reason = "unsupported session type";
break;
}

/* One program start per channel, as RFC 4254 section 6.5 allows. This
* request's grant is recorded once the callback returns, so a flag
* already set is an earlier request's. */
if (!rej && channel->sessionGranted) {
rej = 1;
reason = "a session is already running on the channel";
}

if (rej) {
wolfSSH_Log(WS_LOG_ERROR,
"[SSHD] Refusing session request from %s: %s [%s]",
conn->ip, reason, cmd != NULL ? cmd : "");
}

return rej;
}


#if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE)
/* Returns 1 only for the store hives that need elevation to write: the three
* LOCAL_MACHINE locations. Every other hive (per-user, per-service,
Expand Down Expand Up @@ -893,6 +979,9 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx,
if (ret == WS_SUCCESS) {
wolfSSH_SetUserAuth(*ctx, DefaultUserAuth);
wolfSSH_SetUserAuthResult(*ctx, UserAuthResult);
wolfSSH_CTX_SetChannelReqShellCb(*ctx, SessionRequestCb);
wolfSSH_CTX_SetChannelReqExecCb(*ctx, SessionRequestCb);
wolfSSH_CTX_SetChannelReqSubsysCb(*ctx, SessionRequestCb);
}

/* set banner to display on connection */
Expand Down Expand Up @@ -3486,6 +3575,7 @@ static void* HandleConnection(void* arg)
/* let UserAuthResult reach this connection to cancel the grace timer
* and to reach conn->auth for the cert force-command */
wolfSSH_SetUserAuthResultCtx(ssh, conn);
wolfSSH_SetChannelReqCtx(ssh, conn);
#if defined(WOLFSSH_OSSH_CERTS) && !defined(_WIN32)
/* Unix-only: each connection is a forked child with its own copy of the
* auth struct. Windows does not enforce OpenSSH certs. */
Expand Down
161 changes: 86 additions & 75 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -4173,6 +4173,18 @@ static void NotifyFwdLocalCleanup(WOLFSSH_CHANNEL* channel)
#endif /* WOLFSSH_FWD */


/* Wipe a command line, which can carry credentials, and free it. */
static void FreeChannelCommand(void* heap, char* command, word32 commandSz)
{
WOLFSSH_UNUSED(heap);

if (command != NULL) {
WS_FORCEZERO(command, commandSz);
WFREE(command, heap, DYNTYPE_STRING);
}
}


void ChannelDelete(WOLFSSH_CHANNEL* channel, void* heap)
{
WOLFSSH_UNUSED(heap);
Expand Down Expand Up @@ -4200,11 +4212,7 @@ void ChannelDelete(WOLFSSH_CHANNEL* channel, void* heap)
channel->channel);
}
ShrinkBuffer(&channel->extDataBuffer, 1);
/* Scrub the peer's command line, which can carry credentials. */
if (channel->command != NULL) {
WS_FORCEZERO(channel->command, channel->commandSz);
WFREE(channel->command, heap, DYNTYPE_STRING);
}
FreeChannelCommand(heap, channel->command, channel->commandSz);
WFREE(channel, heap, DYNTYPE_CHANNEL);
}
}
Expand Down Expand Up @@ -13090,14 +13098,71 @@ static void SetTerminalSize(WOLFSSH* ssh, word32 widthChar, word32 heightRows,
#endif /* WOLFSSH_TERM */


/* Wipe the old command ahead of the GetStringAlloc() that frees it, so a
* repeat request leaves no credentials behind in the freed block. */
static void ScrubChannelCommand(WOLFSSH_CHANNEL* channel)
/* Answers a shell, exec, or subsystem request. Sets the session type and
* command for the callback to read, and keeps them only if it accepts. */
static int DoChannelRequestSession(WOLFSSH* ssh, word32 channelId,
WOLFSSH_CHANNEL* channel, byte sessionType, WS_CallbackChannelReq cb,
byte* buf, word32 len, word32* idx, int* rej)
{
if (channel->command != NULL) {
WS_FORCEZERO(channel->command, channel->commandSz);
channel->commandSz = 0;
void* heap = ssh->ctx->heap;
byte prevType = channel->sessionType;
byte hasCommand = (sessionType != WOLFSSH_SESSION_SHELL);
char* prevCommand = NULL;
word32 prevCommandSz = 0;
char* command = NULL;
word32 commandSz = 0;
int ret = WS_SUCCESS;

/* A shell request carries no command, so it leaves the old one alone.
* The others read into a local, so the old survives a refusal. */
if (hasCommand) {
prevCommand = channel->command;
prevCommandSz = channel->commandSz;

ret = GetStringAlloc(heap, &command, &commandSz, buf, len, idx);
if (ret == WS_SUCCESS)
WLOG(WS_LOG_DEBUG, " command = %s", command);
else
WLOG(WS_LOG_DEBUG, " command = %s", "<bad value>");
}

if (ret == WS_SUCCESS) {
if (hasCommand) {
channel->command = command;
channel->commandSz = commandSz;
}
channel->sessionType = sessionType;

if (cb != NULL)
*rej = cb(channel, ssh->channelReqCtx);
else
*rej = ssh->appChannels;

/* A callback may free its own channel, so look it up again. */
channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF);
if (channel == NULL) {
/* The new command went with it. */
FreeChannelCommand(heap, prevCommand, prevCommandSz);
return ret;
}
}

if (ret == WS_SUCCESS && !*rej) {
FreeChannelCommand(heap, prevCommand, prevCommandSz);
channel->sessionGranted = 1;
ssh->clientState = CLIENT_DONE;
}
else {
/* A refusal changes nothing, so an earlier grant still stands. */
if (hasCommand) {
FreeChannelCommand(heap, command, commandSz);
channel->command = prevCommand;
channel->commandSz = prevCommandSz;
}
channel->sessionType = prevType;
}

return ret;
}


Expand All @@ -13110,7 +13175,7 @@ static int DoChannelRequest(WOLFSSH* ssh,
word32 typeSz;
char type[32];
byte wantReply;
int ret, rej = 0, sessionReq = 0;
int ret, rej = 0;

WLOG(WS_LOG_DEBUG, "Entering DoChannelRequest()");

Expand Down Expand Up @@ -13160,59 +13225,19 @@ static int DoChannelRequest(WOLFSSH* ssh,
}
}
else if (ChannelRequestIs(type, typeSz, "shell")) {
channel->sessionType = WOLFSSH_SESSION_SHELL;
if (ssh->ctx->channelReqShellCb) {
rej = ssh->ctx->channelReqShellCb(channel, ssh->channelReqCtx);
}
else {
rej = ssh->appChannels;
}
sessionReq = 1;
ssh->clientState = CLIENT_DONE;
ret = DoChannelRequestSession(ssh, channelId, channel,
WOLFSSH_SESSION_SHELL, ssh->ctx->channelReqShellCb,
buf, len, &begin, &rej);
}
else if (ChannelRequestIs(type, typeSz, "exec")) {
ScrubChannelCommand(channel);
ret = GetStringAlloc(ssh->ctx->heap,
&channel->command, &channel->commandSz,
buf, len, &begin);
if (ret == WS_SUCCESS)
WLOG(WS_LOG_DEBUG, " command = %s", channel->command);
else
WLOG(WS_LOG_DEBUG, " command = %s", "<bad value>");
if (ret == WS_SUCCESS) {
channel->sessionType = WOLFSSH_SESSION_EXEC;
if (ssh->ctx->channelReqExecCb) {
rej = ssh->ctx->channelReqExecCb(channel,
ssh->channelReqCtx);
}
else {
rej = ssh->appChannels;
}
}
sessionReq = 1;
ssh->clientState = CLIENT_DONE;
ret = DoChannelRequestSession(ssh, channelId, channel,
WOLFSSH_SESSION_EXEC, ssh->ctx->channelReqExecCb,
buf, len, &begin, &rej);
}
else if (ChannelRequestIs(type, typeSz, "subsystem")) {
ScrubChannelCommand(channel);
ret = GetStringAlloc(ssh->ctx->heap,
&channel->command, &channel->commandSz,
buf, len, &begin);
if (ret == WS_SUCCESS)
WLOG(WS_LOG_DEBUG, " subsystem = %s", channel->command);
else
WLOG(WS_LOG_DEBUG, " subsystem = %s", "<bad value>");
if (ret == WS_SUCCESS) {
channel->sessionType = WOLFSSH_SESSION_SUBSYSTEM;
if (ssh->ctx->channelReqSubsysCb) {
rej = ssh->ctx->channelReqSubsysCb(channel,
ssh->channelReqCtx);
}
else {
rej = ssh->appChannels;
}
}
sessionReq = 1;
ssh->clientState = CLIENT_DONE;
ret = DoChannelRequestSession(ssh, channelId, channel,
WOLFSSH_SESSION_SUBSYSTEM, ssh->ctx->channelReqSubsysCb,
buf, len, &begin, &rej);
}
#ifdef WOLFSSH_TERM
else if (ChannelRequestIs(type, typeSz, "pty-req")) {
Expand Down Expand Up @@ -13347,20 +13372,6 @@ static int DoChannelRequest(WOLFSSH* ssh,
*idx = len;
}

/* Record the answer, not the ask: sessionType and command are set before
* the reject decision and stay set on a refusal, so they cannot say
* whether the session was granted. Set even without a wantReply, which
* changes only whether the peer is told.
*
* Look the channel up again rather than reusing the pointer from
* before the callback. A callback may close its own channel, and
* wolfSSH_ChannelFree() frees it, so the old pointer can be dead. */
if (sessionReq) {
channel = ChannelFind(ssh, channelId, WS_CHANNEL_ID_SELF);
if (channel != NULL)
channel->sessionGranted = (ret == WS_SUCCESS && !rej);
}

if (wantReply) {
int replyRet;

Expand Down
Loading
Loading