From b22da3386d22d180b3a60ef927e1c5872eb6a578 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 14 Sep 2026 15:02:39 -0700 Subject: [PATCH 1/4] internal: log a subsystem request as a subsystem DoChannelRequestSession() serves the shell, exec and subsystem arms together, and its debug line labels the string it read. A subsystem request labels that string "subsystem" rather than "command". - unit.c: renumber the shell-after-exec failure to follow the codes above it --- src/internal.c | 8 ++++++-- tests/unit.c | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/internal.c b/src/internal.c index 5d1d61124..8ff47898b 100644 --- a/src/internal.c +++ b/src/internal.c @@ -13123,9 +13123,13 @@ static int DoChannelRequestSession(WOLFSSH* ssh, word32 channelId, ret = GetStringAlloc(heap, &command, &commandSz, buf, len, idx); if (ret == WS_SUCCESS) - WLOG(WS_LOG_DEBUG, " command = %s", command); + WLOG(WS_LOG_DEBUG, " %s = %s", + sessionType == WOLFSSH_SESSION_SUBSYSTEM + ? "subsystem" : "command", command); else - WLOG(WS_LOG_DEBUG, " command = %s", ""); + WLOG(WS_LOG_DEBUG, " %s = %s", + sessionType == WOLFSSH_SESSION_SUBSYSTEM + ? "subsystem" : "command", ""); } if (ret == WS_SUCCESS) { diff --git a/tests/unit.c b/tests/unit.c index 392503d14..e8ddc1d2a 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -10574,7 +10574,7 @@ static int test_DoChannelRequest(void) if (wolfSSH_TestDoChannelRequest(ssh, (byte*)payShell, (word32)sizeof(payShell), &idxShell) != WS_SUCCESS) { printf("DoChannelRequest[shell-after-exec]: failed\n"); - result = -500; + result = -512; goto done; } cmd = wolfSSH_ChannelGetSessionCommand(ch); From 7a8ff751f5ed994a53a01202887a5e9dec6d2c3f Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 2 Sep 2026 12:01:19 -0700 Subject: [PATCH 2/4] wolfsshd: refuse sessions it cannot serve A shell, exec or subsystem request is answered as it arrives, through the channel request callbacks, so a session this build cannot serve, or a second one on a channel already running one, is refused with CHANNEL_FAILURE rather than accepted and then dropped once the session is up. What the daemon serves is unchanged. - SessionRequestCb() takes a shell with WOLFSSH_SHELL, an exec with WOLFSSH_SHELL or an scp command with WOLFSSH_SCP, and the sftp subsystem with WOLFSSH_SFTP; anything else is refused and logged - a request whose command did not fit is refused rather than read through a NULL - a second program start is refused on a channel whose grant already stands, so sftp or scp cannot take over a running session - the sftp name is matched whole and scp only as its own token, both by length and bytes, so "scpbackup" or a name with an embedded NUL is some other command - sshd_bad_subsystem_test.sh asks for an unknown subsystem with the OpenSSH client and expects the refusal --- apps/wolfsshd/test/run_all_sshd_tests.sh | 1 + apps/wolfsshd/test/sshd_bad_subsystem_test.sh | 90 +++++++++++++++++ apps/wolfsshd/wolfsshd.c | 99 +++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100755 apps/wolfsshd/test/sshd_bad_subsystem_test.sh diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index 78c76f463..fb7395d5f 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -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" diff --git a/apps/wolfsshd/test/sshd_bad_subsystem_test.sh b/apps/wolfsshd/test/sshd_bad_subsystem_test.sh new file mode 100755 index 000000000..bf91f5f2d --- /dev/null +++ b/apps/wolfsshd/test/sshd_bad_subsystem_test.sh @@ -0,0 +1,90 @@ +#!/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="$3" +if [ -z "$USER" ]; then + USER=`whoami` +fi +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 + +# The regression this test looks for is a request the daemon never answers, +# which leaves the client waiting. Bound every call so that hangs the test +# rather than the suite. +if ! command -v timeout >/dev/null 2>&1; then + echo "timeout not found, skipping" + exit 77 +fi +TIMEOUT="timeout 20" + +# OpenSSH refuses a key file other users can read. +KEY=`mktemp 2>/dev/null` || KEY=`mktemp -t sshdbadsubsys` +OUT=`mktemp 2>/dev/null` || OUT=`mktemp -t sshdbadsubsysout` +if [ -z "$KEY" ] || [ ! -f "$KEY" ] || [ -z "$OUT" ] || [ ! -f "$OUT" ]; then + echo "could not create temp files" + rm -f "$KEY" "$OUT" + exit 1 +fi +trap 'rm -f "$KEY" "$OUT"' EXIT + +cat "$PRIVATE_KEY" > "$KEY" || exit 1 +chmod 600 "$KEY" + +ssh_to_sshd() { + $TIMEOUT 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" + exit 1 +fi + +# A subsystem nothing serves: the client reports the refusal and exits +# non-zero. Check the timeout first, its 124 is non-zero too but means the +# request went unanswered, the opposite of what this test wants. +ssh_to_sshd "$1" "$2" -s no-such-subsystem > "$OUT" 2>&1 +RESULT=$? +if [ "$RESULT" = "124" ]; then + echo "The unknown subsystem request went unanswered:" + cat "$OUT" + exit 1 +fi +if [ "$RESULT" = "0" ]; then + echo "Expecting the unknown subsystem request to fail" + cat "$OUT" + exit 1 +fi +if ! grep -q "subsystem request failed" "$OUT"; then + echo "Expecting the client to report the refused subsystem request:" + cat "$OUT" + exit 1 +fi + +cd "$TESTDIR" +exit 0 diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index a3d0389d2..218b4b19a 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -471,6 +471,101 @@ 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 + { + word32 cmdSz = wolfSSH_ChannelGetSessionCommandSz(channel); + + /* "scp" must stand as its own token; a prefix match + * grants "scpbackup", and a NUL makes it another. */ + if (cmdSz >= (word32)WSTRLEN("scp") + && WSTRNCMP(cmd, "scp", 3) == 0 + && (cmdSz == (word32)WSTRLEN("scp") + || cmd[3] == ' ')) { + 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, @@ -893,6 +988,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 */ @@ -3511,6 +3609,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. */ From c995db910c6afc3a515cd577ed1c0e676bba8928 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Tue, 15 Sep 2026 19:57:06 -0700 Subject: [PATCH 3/4] scp: match the scp command as a whole token wolfSSH_ChannelCommandIsScp() reports whether a channel's session command starts an SCP transfer, taking "scp" only as its own token. The divert in wolfSSH_accept() and an application's exec callback both ask it, so the two cannot disagree about what the SCP server is handed. - an exec of "scpbackup foo" runs as an ordinary exec - a command carrying a NUL within the recorded command size is not an SCP command; ParseScpCommand() walks a C string, so a NUL would drop whatever follows it - wolfsshd's session callback asks through the same helper --- apps/wolfsshd/wolfsshd.c | 17 +++------- src/ssh.c | 3 +- src/wolfscp.c | 30 ++++++++++++----- tests/regress.c | 73 ++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 1 - wolfssh/wolfscp.h | 14 ++++++++ 6 files changed, 116 insertions(+), 22 deletions(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 218b4b19a..a46a3ae28 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -502,18 +502,11 @@ static int SessionRequestCb(WOLFSSH_CHANNEL* channel, void* vCtx) break; } #ifdef WOLFSSH_SCP - { - word32 cmdSz = wolfSSH_ChannelGetSessionCommandSz(channel); - - /* "scp" must stand as its own token; a prefix match - * grants "scpbackup", and a NUL makes it another. */ - if (cmdSz >= (word32)WSTRLEN("scp") - && WSTRNCMP(cmd, "scp", 3) == 0 - && (cmdSz == (word32)WSTRLEN("scp") - || cmd[3] == ' ')) { - rej = 0; - break; - } + /* Shared with the SCP divert in wolfSSH_accept(), so the two + * cannot disagree about what starts a transfer. */ + if (wolfSSH_ChannelCommandIsScp(channel) == 1) { + rej = 0; + break; } #endif #ifdef WOLFSSH_SHELL diff --git a/src/ssh.c b/src/ssh.c index 17054f834..625f88bbc 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -818,7 +818,8 @@ int wolfSSH_accept(WOLFSSH* ssh) #ifdef WOLFSSH_SCP if (ssh->channelList != NULL && ssh->channelList->sessionGranted - && ChannelCommandIsScp(ssh)) { + && wolfSSH_ChannelCommandIsScp(ssh->channelList) + == 1) { ssh->acceptState = ACCEPT_INIT_SCP_TRANSFER; WLOG(WS_LOG_DEBUG, acceptState, "ACCEPT_INIT_SCP_TRANSFER"); return WS_SCP_INIT; diff --git a/src/wolfscp.c b/src/wolfscp.c index cbbeb7044..7d69cd5de 100644 --- a/src/wolfscp.c +++ b/src/wolfscp.c @@ -1040,20 +1040,34 @@ WOLFSSH_API int wolfSSH_SetScpErrorMsg(WOLFSSH* ssh, const char* message) return ret; } -/* Determine if channel command sent in initial negotiation is scp. - * Return 1 if yes, 0 if no */ -int ChannelCommandIsScp(WOLFSSH* ssh) +/* Determine if the channel's session command is scp. See wolfscp.h for + * the contract; "scp" must stand as its own token. */ +int wolfSSH_ChannelCommandIsScp(const WOLFSSH_CHANNEL* channel) { const char* cmd; + word32 cmdSz; + word32 scpSz = (word32)WSTRLEN("scp"); + word32 i; int ret = 0; - if (ssh == NULL) + if (channel == NULL) return WS_BAD_ARGUMENT; - cmd = wolfSSH_GetSessionCommand(ssh); - if (cmd != NULL && WSTRLEN(cmd) >= 3) { - if (WSTRNCMP(cmd, "scp", 3) == 0) - ret = 1; + cmd = wolfSSH_ChannelGetSessionCommand(channel); + cmdSz = wolfSSH_ChannelGetSessionCommandSz(channel); + + if (cmd != NULL && cmdSz >= scpSz + && WSTRNCMP(cmd, "scp", scpSz) == 0 + && (cmdSz == scpSz || cmd[scpSz] == ' ')) { + ret = 1; + } + + /* The parse that follows is a C string walk, so a NUL inside the + * command would drop the rest of it. Refuse rather than transfer + * something other than what was asked for. */ + for (i = 0; ret == 1 && i < cmdSz; i++) { + if (cmd[i] == '\0') + ret = 0; } return ret; diff --git a/tests/regress.c b/tests/regress.c index eaa841542..718c6ad3e 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -4833,6 +4833,78 @@ static void TestAcceptDivertNeedsScpGrantAlone(void) FreeChannelOpenHarness(&harness); } + +/* The SCP divert reads only the command, so "scp" has to stand as its own + * token: a bare prefix match takes "scpbackup" into the built-in SCP + * server. A NUL anywhere in the command hides what follows it from the + * parse that serves the transfer, whether it sits where the space belongs + * or past a good token boundary. wolfsshd's exec callback asks the same + * question with the same helper, so the two cannot answer it + * differently. */ +static void TestAcceptDivertMatchesScpCommandToken(void) +{ + static const struct { + const char* cmd; + word32 cmdSz; + int divert; + } cases[] = { + { "scp -f /tmp/a", 13, 1 }, /* what the real client sends */ + { "scp", 3, 1 }, /* the token alone */ + { "scpbackup foo", 13, 0 }, /* the token is only a prefix */ + { "scp\0 -f /tmp/a", 14, 0 }, /* a NUL where the space belongs */ + { "scp -t /d\0x", 11, 0 }, /* a NUL hiding a tail past the token */ + { "scp -t /d\0", 10, 0 }, /* a NUL counted at the end */ + { "sc", 2, 0 }, /* shorter than the token */ + { "ls", 2, 0 } + }; + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + byte payload[128]; + byte in[128]; + word32 idx; + word32 inSz; + word32 i; + + AssertIntEQ(wolfSSH_ChannelCommandIsScp(NULL), WS_BAD_ARGUMENT); + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + InitChannelOpenHarness(&harness, NULL, 0); + channel = SeedUnconfirmedChannel(&harness); + AssertIntEQ(ChannelUpdatePeer(channel, 5, 1024, 1024), WS_SUCCESS); + channel->openConfirmed = 1; + + idx = 0; + idx = AppendUint32(payload, sizeof(payload), idx, channel->channel); + idx = AppendString(payload, sizeof(payload), idx, "exec"); + idx = AppendByte(payload, sizeof(payload), idx, 1); + idx = AppendUint32(payload, sizeof(payload), idx, cases[i].cmdSz); + idx = AppendData(payload, sizeof(payload), idx, + (const byte*)cases[i].cmd, cases[i].cmdSz); + inSz = WrapPacket(MSGID_CHANNEL_REQUEST, payload, idx, in, sizeof(in)); + RepointHarnessInput(&harness, in, inSz); + + /* Neither app-channels nor a callback, so the request is granted + * and the session is the one wolfSSH_accept() goes on to serve. */ + AssertIntEQ(DoReceive(harness.ssh), WS_SUCCESS); + AssertIntEQ(channel->commandSz, cases[i].cmdSz); + AssertIntEQ(wolfSSH_ChannelCommandIsScp(channel), cases[i].divert); + RepointHarnessInput(&harness, NULL, 0); + + harness.ssh->acceptState = ACCEPT_SERVER_CHANNEL_ACCEPT_SENT; + if (cases[i].divert) { + AssertIntEQ(wolfSSH_accept(harness.ssh), WS_SCP_INIT); + AssertIntEQ(harness.ssh->acceptState, ACCEPT_INIT_SCP_TRANSFER); + } + else { + AssertIntEQ(wolfSSH_accept(harness.ssh), WS_SUCCESS); + AssertIntEQ(harness.ssh->acceptState, + ACCEPT_CLIENT_SESSION_ESTABLISHED); + } + + FreeChannelOpenHarness(&harness); + } +} + #endif /* WOLFSSH_SCP */ /* A username change after the first userauth request must end the session. */ @@ -14921,6 +14993,7 @@ int main(int argc, char** argv) TestAcceptDivertNeedsSftpGrantAlone(); #endif #ifdef WOLFSSH_SCP + TestAcceptDivertMatchesScpCommandToken(); TestAcceptDivertNeedsScpGrant(); TestAcceptDivertNeedsScpGrantAlone(); #endif diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 706c6fc51..51eae21ca 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -2257,7 +2257,6 @@ enum WS_ScpDirection { WOLFSSH_SCP_FROM }; -WOLFSSH_LOCAL int ChannelCommandIsScp(WOLFSSH* ssh); WOLFSSH_LOCAL int DoScpRequest(WOLFSSH* ssh); WOLFSSH_LOCAL int DoScpSink(WOLFSSH* ssh); WOLFSSH_LOCAL int DoScpSource(WOLFSSH* ssh); diff --git a/wolfssh/wolfscp.h b/wolfssh/wolfscp.h index 1ebeb339f..84767aaed 100644 --- a/wolfssh/wolfscp.h +++ b/wolfssh/wolfscp.h @@ -170,6 +170,20 @@ WOLFSSH_API int wolfSSH_SCP_from(WOLFSSH* ssh, const char* src, * done; call it again on the same session until it completes. */ WOLFSSH_API int wolfSSH_SCP_accept(WOLFSSH* ssh); +/* Tells an SCP request from an ordinary exec, for use from an exec + * channel-request callback and by wolfSSH_accept() itself, so the two + * cannot disagree about what starts a transfer. + * + * Returns 1 when the channel's session command starts an SCP transfer, 0 + * when it does not, and WS_BAD_ARGUMENT when channel is NULL. "scp" must + * stand as its own token: a bare prefix match would take "scpbackup" as a + * transfer. The test runs over the recorded command size rather than the + * string length, and a command carrying a NUL anywhere within that size + * is not an SCP command: the parse that serves the transfer walks a C + * string, so a NUL would silently drop whatever follows it. */ +WOLFSSH_API int wolfSSH_ChannelCommandIsScp( + const WOLFSSH_CHANNEL* channel); + #ifdef __cplusplus } From f31d9c5a35032dfc7c714eb49b1f4bae61cb9f81 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Tue, 15 Sep 2026 19:57:27 -0700 Subject: [PATCH 4/4] ssh: add a channel session-granted accessor wolfSSH_ChannelGetSessionGranted() reports whether a shell, exec or subsystem request on a channel has been answered CHANNEL_SUCCESS, so an application can tell a second request from the first without reaching into WOLFSSH_CHANNEL. - the flag is still clear for the request a session callback is answering, so a set flag is an earlier request's grant - wolfsshd's session callback reads the grant through it --- apps/wolfsshd/wolfsshd.c | 2 +- src/ssh.c | 11 +++++++++++ tests/regress.c | 32 ++++++++++++++++++++++++++++++++ wolfssh/ssh.h | 6 ++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index a46a3ae28..045ac5ea0 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -544,7 +544,7 @@ static int SessionRequestCb(WOLFSSH_CHANNEL* channel, void* vCtx) /* 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) { + if (!rej && wolfSSH_ChannelGetSessionGranted(channel) == 1) { rej = 1; reason = "a session is already running on the channel"; } diff --git a/src/ssh.c b/src/ssh.c index 625f88bbc..472de2bae 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -5761,6 +5761,17 @@ word32 wolfSSH_ChannelGetSessionCommandSz(const WOLFSSH_CHANNEL* channel) } +/* returns 1 if a session was granted on the channel, 0 if not, and + * negative on failure */ +int wolfSSH_ChannelGetSessionGranted(const WOLFSSH_CHANNEL* channel) +{ + if (channel == NULL) { + return WS_BAD_ARGUMENT; + } + return channel->sessionGranted; +} + + int wolfSSH_CTX_SetChannelOpenCb(WOLFSSH_CTX* ctx, WS_CallbackChannelOpen cb) { int ret = WS_SSH_CTX_NULL_E; diff --git a/tests/regress.c b/tests/regress.c index 718c6ad3e..da37a0717 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -4907,6 +4907,37 @@ static void TestAcceptDivertMatchesScpCommandToken(void) #endif /* WOLFSSH_SCP */ +/* The grant an application reads through the public accessor is the one the + * channel records: clear while the request that would set it is still being + * answered, set once it has been. */ +static void TestChannelGetSessionGrantedAccessor(void) +{ + ChannelOpenHarness harness; + WOLFSSH_CHANNEL* channel; + byte in[128]; + word32 inSz; + + AssertIntEQ(wolfSSH_ChannelGetSessionGranted(NULL), WS_BAD_ARGUMENT); + + InitChannelOpenHarness(&harness, NULL, 0); + channel = SeedUnconfirmedChannel(&harness); + AssertIntEQ(ChannelUpdatePeer(channel, 5, 1024, 1024), WS_SUCCESS); + channel->openConfirmed = 1; + AssertIntEQ(wolfSSH_ChannelGetSessionGranted(channel), 0); + + inSz = BuildChannelStringRequestPacket(channel->channel, "exec", 1, + "ls", in, sizeof(in)); + RepointHarnessInput(&harness, in, inSz); + + AssertIntEQ(DoReceive(harness.ssh), WS_SUCCESS); + AssertIntEQ(wolfSSH_ChannelGetSessionGranted(channel), 1); + AssertIntEQ(wolfSSH_ChannelGetSessionGranted(channel), + channel->sessionGranted); + + FreeChannelOpenHarness(&harness); +} + + /* A username change after the first userauth request must end the session. */ static void TestUsernameChangeDisconnects(void) { @@ -14972,6 +15003,7 @@ int main(int argc, char** argv) TestChannelReqExecCallbackRuns(); TestChannelReqSubsysCallbackRuns(); TestSessionReqCallbackSeesCommandSz(); + TestChannelGetSessionGrantedAccessor(); TestMalformedSessionRequestSkipsCallback(); TestSessionReqCallbackMayFreeChannel(); TestAppChannelsAcceptKeepsStopWithPendingOutput(); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index e2e4b4c77..181c836e8 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -451,6 +451,12 @@ WOLFSSH_API const char* wolfSSH_ChannelGetSessionCommand( WOLFSSH_API word32 wolfSSH_ChannelGetSessionCommandSz( const WOLFSSH_CHANNEL* channel); WOLFSSH_API int wolfSSH_ChannelIsPty(const WOLFSSH_CHANNEL* channel); +/* Returns 1 once a shell, exec or subsystem request on the channel has been + * answered CHANNEL_SUCCESS, 0 while none has been, and WS_BAD_ARGUMENT when + * channel is NULL. A session-request callback sees this still clear for the + * request it is answering, so a set flag is an earlier request's grant. */ +WOLFSSH_API int wolfSSH_ChannelGetSessionGranted( + const WOLFSSH_CHANNEL* channel); /* Channel callbacks */ typedef int (*WS_CallbackChannelOpen)(WOLFSSH_CHANNEL* channel, void* ctx);