Skip to content

Commit c3193c9

Browse files
Apply security audit findings
Signed-off-by: Christian Parpart <christian@parpart.family>
1 parent 99110ce commit c3193c9

File tree

4 files changed

+132
-28
lines changed

4 files changed

+132
-28
lines changed

src/vtbackend/Sequence.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class SequenceParameterBuilder
128128

129129
void nextParameter()
130130
{
131-
if (_currentParameter != _parameters->_values.end())
131+
if (std::next(_currentParameter) != _parameters->_values.end())
132132
{
133133
++_currentParameter;
134134
*_currentParameter = 0;
@@ -138,7 +138,7 @@ class SequenceParameterBuilder
138138

139139
void nextSubParameter()
140140
{
141-
if (_currentParameter != _parameters->_values.end())
141+
if (std::next(_currentParameter) != _parameters->_values.end())
142142
{
143143
++_currentParameter;
144144
*_currentParameter = 0;
@@ -148,7 +148,11 @@ class SequenceParameterBuilder
148148

149149
constexpr void multiplyBy10AndAdd(uint8_t value) noexcept
150150
{
151-
*_currentParameter = static_cast<uint16_t>((*_currentParameter * 10) + value);
151+
unsigned const newValue = (*_currentParameter * 10) + value;
152+
if (newValue > 0xFFFF)
153+
*_currentParameter = 0xFFFF;
154+
else
155+
*_currentParameter = static_cast<uint16_t>(newValue);
152156
}
153157

154158
constexpr void apply(uint16_t value) noexcept

src/vtbackend/Sequence_test.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,30 @@ TEST_CASE("SequenceParameterBuilder.complex.2")
124124
INFO(parameters.subParameterBitString());
125125
CHECK(parameters.str() == "0;12::34:56;7;89");
126126
}
127+
128+
TEST_CASE("SequenceParameterBuilder.max_parameters_exceeded")
129+
{
130+
auto parameters = SequenceParameters {};
131+
auto builder = SequenceParameterBuilder { parameters };
132+
133+
// Fill up to 16 parameters: "1;2;...;16"
134+
for (int i = 1; i <= 16; ++i)
135+
{
136+
if (i > 1)
137+
builder.nextParameter();
138+
builder.apply(i);
139+
}
140+
141+
// Try to add a 17th parameter: ";17"
142+
// This should be ignored (or merged) and NOT cause an OOB write.
143+
builder.nextParameter();
144+
builder.apply(17);
145+
146+
builder.fixiate();
147+
148+
CHECK(parameters.count() == 16);
149+
150+
// Verify behavior: since advancement was blocked, the value '17' was appended to the last parameter '16'.
151+
// 16 -> 1617
152+
CHECK(parameters.at(15) == 1617);
153+
}

src/vtpty/SshSession.cpp

Lines changed: 96 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -502,11 +502,17 @@ void SshSession::processState()
502502
}
503503
case State::VerifyHostKey: {
504504
if (!verifyHostKey())
505+
{
506+
if (_state == State::VerifyHostKeyWaitForInput)
507+
return;
508+
505509
setState(State::Failure);
510+
}
506511
else
507512
setState(State::AuthenticateAgent);
508513
break;
509514
}
515+
case State::VerifyHostKeyWaitForInput: return;
510516
case State::AuthenticateAgent: {
511517
authenticateWithAgent();
512518
break;
@@ -814,6 +820,11 @@ int SshSession::write(std::string_view buf)
814820
handlePreAuthenticationPasswordInput(buf, State::AuthenticatePrivateKey);
815821
return static_cast<int>(buf.size()); // Make the caller believe that we have written all bytes.
816822
}
823+
else if (_state == State::VerifyHostKeyWaitForInput)
824+
{
825+
handlePreAuthenticationPasswordInput(buf, State::VerifyHostKey);
826+
return static_cast<int>(buf.size()); // Make the caller believe that we have written all bytes.
827+
}
817828
else if (_state != State::Operational)
818829
{
819830
sshLog()("Ignoring write() call in state: {}", _state);
@@ -839,13 +850,22 @@ int SshSession::write(std::string_view buf)
839850

840851
if (ptyOutLog)
841852
{
842-
if (rv >= 0)
843-
ptyOutLog()("Sending bytes: \"{}\"", crispy::escape(buf.data(), buf.data() + rv)); // NOLINT
853+
bool const sensitive =
854+
_state >= State::AuthenticatePrivateKeyStart && _state <= State::AuthenticatePassword;
855+
if (sensitive)
856+
{
857+
ptyOutLog()("Sending {} bytes: (hidden in auth state)", rv);
858+
}
859+
else
860+
{
861+
if (rv >= 0)
862+
ptyOutLog()("Sending bytes: \"{}\"", crispy::escape(buf.data(), buf.data() + rv)); // NOLINT
844863

845-
if (0 <= rv && static_cast<size_t>(rv) < buf.size())
846-
ptyOutLog()("Partial write. {} bytes written and {} bytes left.",
847-
rv,
848-
buf.size() - static_cast<size_t>(rv));
864+
if (0 <= rv && static_cast<size_t>(rv) < buf.size())
865+
ptyOutLog()("Partial write. {} bytes written and {} bytes left.",
866+
rv,
867+
buf.size() - static_cast<size_t>(rv));
868+
}
849869
}
850870

851871
return static_cast<int>(rv);
@@ -1088,27 +1108,78 @@ bool SshSession::verifyHostKey()
10881108
logError("Host key verification failed. Host key mismatch.");
10891109
return false;
10901110
case LIBSSH2_KNOWNHOST_CHECK_NOTFOUND: {
1091-
// TODO: Ask user whether to add host key to known_hosts file
1092-
auto const comment =
1093-
std::format("{}@{}:{} (added by Contour)", _config.username, _config.hostname, _config.port);
1094-
auto const typeMask = LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW | knownhostType;
1095-
libssh2_knownhost_addc(knownHosts,
1096-
_config.hostname.c_str(),
1097-
nullptr /* salt */,
1098-
hostkeyRaw,
1099-
hostkeyLength,
1100-
comment.data(),
1101-
comment.size(),
1102-
typeMask,
1103-
nullptr /* store */);
1104-
rc = libssh2_knownhost_writefile(
1105-
knownHosts, _config.knownHostsFile.string().c_str(), LIBSSH2_KNOWNHOST_FILE_OPENSSH);
1106-
if (rc != LIBSSH2_ERROR_NONE)
1111+
if (!_injectedWrite.empty())
11071112
{
1108-
logErrorWithDetails(rc, "Failed to write known_hosts file");
1109-
return false;
1113+
// We've got a user response.
1114+
auto const trim = [](std::string_view s) {
1115+
auto const start = s.find_first_not_of(" \t\r\n");
1116+
if (start == std::string_view::npos)
1117+
return std::string_view {};
1118+
auto const end = s.find_last_not_of(" \t\r\n");
1119+
return s.substr(start, end - start + 1);
1120+
};
1121+
auto const response = trim(_injectedWrite);
1122+
_injectedWrite.clear();
1123+
if (response == "yes")
1124+
{
1125+
auto const comment = std::format(
1126+
"{}@{}:{} (added by Contour)", _config.username, _config.hostname, _config.port);
1127+
auto const typeMask =
1128+
LIBSSH2_KNOWNHOST_TYPE_PLAIN | LIBSSH2_KNOWNHOST_KEYENC_RAW | knownhostType;
1129+
libssh2_knownhost_addc(knownHosts,
1130+
_config.hostname.c_str(),
1131+
nullptr /* salt */,
1132+
hostkeyRaw,
1133+
hostkeyLength,
1134+
comment.data(),
1135+
comment.size(),
1136+
typeMask,
1137+
nullptr /* store */);
1138+
rc = libssh2_knownhost_writefile(
1139+
knownHosts, _config.knownHostsFile.string().c_str(), LIBSSH2_KNOWNHOST_FILE_OPENSSH);
1140+
if (rc != LIBSSH2_ERROR_NONE)
1141+
{
1142+
logErrorWithDetails(rc, "Failed to write known_hosts file");
1143+
return false;
1144+
}
1145+
injectRead("\r\nHost key verified and added.\r\n");
1146+
return true;
1147+
}
1148+
else
1149+
{
1150+
injectRead("\r\nHost key verification failed. Connection aborted.\r\n");
1151+
return false;
1152+
}
11101153
}
1111-
return true;
1154+
// Ask user whether to add host key to known_hosts file
1155+
libssh2_knownhost_get(knownHosts, &knownHost, knownHost); // wait, knownHost is null?
1156+
// libssh2_knownhost_checkp returns the known host if found, but here it is NOTFOUND.
1157+
// We need to generate fingerprint from hostkeyRaw.
1158+
libssh2_session_hostkey(
1159+
_p->sshSession, &hostkeyLength, &knownhostType); // Use session function to get fingerprint?
1160+
// Actually libssh2_hostkey_hash is what we want.
1161+
auto const fingerprint = libssh2_hostkey_hash(_p->sshSession, LIBSSH2_HOSTKEY_HASH_SHA256);
1162+
1163+
auto fingerprintHex = std::string {};
1164+
if (fingerprint)
1165+
{
1166+
// SHA256 is 32 bytes
1167+
for (int i = 0; i < 32; ++i)
1168+
fingerprintHex += std::format("{:02X}:", (unsigned char) fingerprint[i]);
1169+
if (!fingerprintHex.empty())
1170+
fingerprintHex.pop_back();
1171+
}
1172+
else
1173+
fingerprintHex = "UNKNOWN";
1174+
1175+
auto const message = std::format("\r\nThe authenticity of host '{}' cannot be established.\r\n"
1176+
"Fingerprint: {}\r\n"
1177+
"Are you sure you want to continue connecting (yes/no)? ",
1178+
_config.hostname,
1179+
fingerprintHex);
1180+
injectRead(message);
1181+
setState(State::VerifyHostKeyWaitForInput);
1182+
return false;
11121183
}
11131184
case LIBSSH2_KNOWNHOST_CHECK_FAILURE: {
11141185
logErrorWithDetails(rc, "Host key verification failed");

src/vtpty/SshSession.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class SshSession final: public Pty
7878
Connect, // connect to SSH server (usually TCP/IPv4 or TCP/IPv6)
7979
Handshake, // SSH handshake
8080
VerifyHostKey, // verify host key against known_hosts file
81+
VerifyHostKeyWaitForInput, // wait for user confirmation of host key
8182
AuthenticateAgent, // authenticate with SSH agent
8283
AuthenticatePrivateKeyStart, // start private key authentication
8384
AuthenticatePrivateKeyRequest, // request private key's password
@@ -185,6 +186,7 @@ struct std::formatter<vtpty::SshSession::State>: std::formatter<std::string_view
185186
case vtpty::SshSession::State::Connect: name = "Connect"; break;
186187
case vtpty::SshSession::State::Handshake: name = "Handshake"; break;
187188
case vtpty::SshSession::State::VerifyHostKey: name = "VerifyHostKey"; break;
189+
case vtpty::SshSession::State::VerifyHostKeyWaitForInput: name = "VerifyHostKeyWaitForInput"; break;
188190
case vtpty::SshSession::State::AuthenticateAgent: name = "AuthenticateAgent"; break;
189191
case vtpty::SshSession::State::AuthenticatePrivateKeyStart: name = "AuthenticatePrivateKeyStart"; break;
190192
case vtpty::SshSession::State::AuthenticatePrivateKeyRequest: name = "AuthenticatePrivateKeyRequest"; break;

0 commit comments

Comments
 (0)