From 6c9aee54ec0475299e4f306a9bafbc7f4c5d901d Mon Sep 17 00:00:00 2001
From: Matt Johnston <matt@ucc.asn.au>
Date: Sun, 31 Dec 2023 12:13:25 +0800
Subject: [PATCH] Only SSH client allows ignored banner lines

The server should expect a banner line straight away, avoids dangling
connections from port scanners etc
---
 src/conn.rs  |  2 +-
 src/ident.rs | 23 ++++++++++++++++++++---
 src/kex.rs   |  4 ++--
 3 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/src/conn.rs b/src/conn.rs
index 1c7ee3a..31fd569 100644
--- a/src/conn.rs
+++ b/src/conn.rs
@@ -100,7 +100,7 @@ impl Conn {
         Ok(Conn {
             sess_id: None,
             kex: Kex::new(),
-            remote_version: ident::RemoteVersion::new(),
+            remote_version: ident::RemoteVersion::new(cliserv.is_client()),
             state: ConnState::SendIdent,
             algo_conf,
             cliserv,
diff --git a/src/ident.rs b/src/ident.rs
index 49f28d3..b38b214 100644
--- a/src/ident.rs
+++ b/src/ident.rs
@@ -33,6 +33,7 @@ pub struct RemoteVersion {
     /// Parse state
     st: VersPars,
     num_lines: usize,
+    is_client: bool,
 }
 
 /// Version parsing state.
@@ -59,11 +60,12 @@ pub(crate) enum VersPars {
 }
 
 impl RemoteVersion {
-    pub fn new() -> Self {
+    pub fn new(is_client: bool) -> Self {
         RemoteVersion {
             storage: [0; MAX_REMOTE_VERSION_LEN],
             st: VersPars::Start(0),
             num_lines: 0,
+            is_client,
         }
     }
 
@@ -113,7 +115,8 @@ impl RemoteVersion {
                     if b == LF {
                         self.st = VersPars::Start(0);
                         self.num_lines += 1;
-                        if self.num_lines > MAX_LINES {
+                        // only client allows extra unknown lines
+                        if !self.is_client || self.num_lines > MAX_LINES {
                             return Err(Error::NotSSH);
                         }
                     }
@@ -160,8 +163,9 @@ mod tests {
     use crate::error::{Error,TrapBug,Result};
     use crate::sunsetlog::init_test_log;
 
+    // Tests as a client, allowing leading ignored lines
     fn test_version(v: &str, split: usize, expect: &str) -> Result<usize> {
-        let mut r = ident::RemoteVersion::new();
+        let mut r = ident::RemoteVersion::new(true);
 
         let split = split.min(v.len());
         let (a, b) = v.as_bytes().split_at(split);
@@ -212,6 +216,19 @@ mod tests {
         Ok(())
     }
 
+
+    #[test]
+    /// check server doesn't allow leading lines
+    fn version_server_lines() -> Result<()> {
+        // server instance, is_client false
+        let mut r = ident::RemoteVersion::new(false);
+        r.consume(b"SSH-2.0-aaa").unwrap();
+        let mut r = ident::RemoteVersion::new(false);
+        // disallow leading lines
+        r.consume(b"zzz\x0d\x0aSSH-2.0-aaa").unwrap_err();
+        Ok(())
+    }
+
     // // TODO: maybe fuzzing would work better.
     // // also hits an ICE, perhaps
     // // https://github.com/rust-lang/rust/pull/94391
diff --git a/src/kex.rs b/src/kex.rs
index f9e81f0..4d80fca 100644
--- a/src/kex.rs
+++ b/src/kex.rs
@@ -896,7 +896,7 @@ mod tests {
     impl TrafCatcher {
         fn new() -> Self {
             let traf_in = traffic::TrafIn::new(vec![0u8; 3000].leak());
-            let mut rv = RemoteVersion::new();
+            let mut rv = RemoteVersion::new(false);
             rv.consume(b"SSH-2.0-thing\r\n").unwrap();
             rv.version().unwrap();
 
@@ -958,7 +958,7 @@ mod tests {
         // needs to be hardcoded because that's what we send.
         let mut s = Vec::from(crate::ident::OUR_VERSION);
         s.extend_from_slice(b"\r\n");
-        let mut version = RemoteVersion::new();
+        let mut version = RemoteVersion::new(true);
         version.consume(s.as_slice()).unwrap();
 
         let mut keys = vec![];
-- 
GitLab