]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #4808: imap: abort fallback functionality
authorUmang Sharma (umasharm) <umasharm@cisco.com>
Thu, 14 Aug 2025 21:08:21 +0000 (21:08 +0000)
committerChris Sherwin (chsherwi) <chsherwi@cisco.com>
Thu, 14 Aug 2025 21:08:21 +0000 (21:08 +0000)
Merge in SNORT/snort3 from ~UMASHARM/snort3:imap_abort to master

Squashed commit of the following:

commit 4dac91772f004283b3ea40ab1428def2483adf7a
Author: Umang Sharma <umasharm@cisco.com>
Date:   Tue Jun 10 15:10:21 2025 -0400

    imap: abort fallback functionality

src/service_inspectors/imap/imap.cc
src/service_inspectors/imap/imap.h
src/service_inspectors/imap/imap_paf.cc
src/service_inspectors/imap/imap_paf.h

index 89abfc38437442e8fa51b8eccc4ddf4b03c3a328..4ca71f93fa0f3d1fee10e847341daa2c3890b09b 100644 (file)
@@ -113,14 +113,27 @@ IMAPToken imap_resps[] =
     { "RECENT",          6, RESP_RECENT },
     { "EXPUNGE",         7, RESP_EXPUNGE },
     { "FETCH",           5, RESP_FETCH },
+    { "LOGIN",           5, RESP_LOGIN },
+    { "LOGOUT",          6, RESP_LOGOUT },
+    { "SELECT",          6, RESP_SELECT },
+    { "EXAMINE",         7, RESP_EXAMINE },
+    { "CREATE",          6, RESP_CREATE },
+    { "DELETE",          6, RESP_DELETE },
+    { "RENAME",          6, RESP_RENAME },
+    { "SUBSCRIBE",       9, RESP_SUBSCRIBE },
+    { "UNSUBSCRIBE",    11, RESP_UNSUBSCRIBE },
+    { "APPEND",          6, RESP_APPEND },
+    { "COPY",            4, RESP_COPY },
     { "BAD",             3, RESP_BAD },
     { "BYE",             3, RESP_BYE },
     { "NO",              2, RESP_NO },
     { "OK",              2, RESP_OK },
     { "PREAUTH",         7, RESP_PREAUTH },
     { "ENVELOPE",        8, RESP_ENVELOPE },
+    { "NAMESPACE",       9, RESP_NAMESPACE },
     { "UID",             3, RESP_UID },
-    { nullptr,   0,  0 }
+
+    { nullptr,           0, 0 }
 };
 
 SearchTool* imap_resp_search_mpse = nullptr;
index 637bca2768fd75a73019f3a5cc61eeb5222f3ca1..264590d6358e7e6fafc45cf7e2b29e81366d9dc4 100644 (file)
@@ -111,12 +111,24 @@ typedef enum _IMAPRespEnum
     RESP_RECENT,
     RESP_EXPUNGE,
     RESP_FETCH,
+    RESP_LOGIN,        
+    RESP_LOGOUT,       
+    RESP_SELECT,      
+    RESP_EXAMINE,     
+    RESP_CREATE,  
+    RESP_DELETE,      
+    RESP_RENAME,       
+    RESP_SUBSCRIBE,    
+    RESP_UNSUBSCRIBE,  
+    RESP_APPEND,       
+    RESP_COPY,        
     RESP_BAD,
     RESP_BYE,
     RESP_NO,
     RESP_OK,
     RESP_PREAUTH,
     RESP_ENVELOPE,
+    RESP_NAMESPACE,
     RESP_UID,
     RESP_LAST
 } IMAPRespEnum;
index 363cc61d443f31ee0ffdf1fef8b671cde4803750..82da737e9c1869542747a932bef975e9bad3a9d0 100644 (file)
@@ -47,11 +47,16 @@ static inline void reset_data_states(ImapPafData* pfdata)
 
     // reset server info
     pfdata->imap_state = IMAP_PAF_CMD_IDENTIFIER;
+    pfdata->data_end_state = IMAP_PAF_DATA_END_UNKNOWN;
+    pfdata->end_of_data = false;
 
-    // reset fetch data information information
+    // reset fetch data information
     pfdata->imap_data_info.paren_cnt = 0;
     pfdata->imap_data_info.next_letter = nullptr;
+    pfdata->imap_data_info.found_len = false;
     pfdata->imap_data_info.length = 0;
+    pfdata->imap_data_info.esc_nxt_char = false;
+    pfdata->imap_data_info.cmd = nullptr;
 }
 
 static inline bool is_untagged(const uint8_t ch)
@@ -214,6 +219,38 @@ static bool find_data_end_mime_data(const uint8_t ch, ImapPafData* pfdata)
     return false;
 }
 
+/*
+ * This function only does something when the character is a blank or a CR/LF.
+ * In those specific cases, this function will set the appropriate next
+ * state information
+ *
+ * PARAMS:
+ *        const uint8_t ch - the next character to analyze.
+ *        ImapPafData *pfdata - the struct containing all imap paf information
+ *        ImapPafData base_state - if a space is not found, revert to this state
+ *        ImapPafData next_state - if a space is found, go to this state
+ * RETURNS:
+ *        true - if the status has been eaten
+ *        false - if a CR or LF has been found
+ */
+static inline void eat_character(const uint8_t ch, ImapPafData* pfdata,
+    ImapPafState base_state, ImapPafState next_state)
+{
+    switch (ch)
+    {
+    case ' ':
+    case '\t':
+    case '[':
+        pfdata->imap_state = next_state;
+        break;
+
+    case '\r':
+    case '\n':
+        pfdata->imap_state = base_state;
+        break;
+    }
+}
+
 /*
  * Initial command processing function.  Determine if this command
  * may be analyzed irregularly ( which currently means if emails
@@ -223,14 +260,134 @@ static bool find_data_end_mime_data(const uint8_t ch, ImapPafData* pfdata)
  *        const uint8_t ch - the next character to analyze.
  *        ImapPafData *pfdata - the struct containing all imap paf information
  */
-static inline void init_command_search(const uint8_t ch, ImapPafData* pfdata)
+static inline void init_command_search(const uint8_t ch, const uint8_t next_ch, ImapPafData* pfdata)
 {
     switch (ch)
     {
+    case '[':
+         eat_character(ch, pfdata, IMAP_PAF_REG_STATE, IMAP_PAF_CMD_SEARCH);
+         break;
     case 'F':
     case 'f':
-        // may be a FETCH response
-        pfdata->imap_data_info.next_letter = &(imap_resps[RESP_FETCH].name[1]);
+        if (pfdata->imap_state == IMAP_PAF_CMD_STATUS)
+        {
+            pfdata->imap_data_info.next_letter = &(imap_resps[RESP_FLAGS].name[1]);
+        }
+        else if (pfdata->imap_state == IMAP_PAF_CMD_SEARCH)
+        {
+            pfdata->imap_data_info.next_letter = &(imap_resps[RESP_FETCH].name[1]);
+            pfdata->imap_data_info.cmd = imap_resps[RESP_FETCH].name;
+        }
+        break;
+
+    case 'N':
+    case 'n':
+        if (imap_resps[RESP_NAMESPACE].name[1] == next_ch)
+        {
+            pfdata->imap_data_info.next_letter = &(imap_resps[RESP_NAMESPACE].name[1]);
+        }
+        break; 
+
+    case 'L':
+    case 'l':
+            if (imap_resps[RESP_LIST].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_LIST].name[1]);
+            }
+            else if (imap_resps[RESP_LSUB].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_LSUB].name[1]);
+            }
+        break;
+
+    case 'S':
+    case 's':
+            if (imap_resps[RESP_SEARCH].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_SEARCH].name[1]);
+            }
+            else if (imap_resps[RESP_STATUS].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_STATUS].name[1]);
+            }
+            else if (imap_resps[RESP_SUBSCRIBE].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_SUBSCRIBE].name[1]);
+            }
+        break;
+
+    case 'C':
+    case 'c':
+            if (imap_resps[RESP_CREATE].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_CREATE].name[1]);
+            }
+            else if (imap_resps[RESP_CAPABILITY].name[1] == next_ch) 
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_CAPABILITY].name[1]);
+            }
+        break;
+
+    case 'D':
+    case 'd':
+            if (imap_resps[RESP_DELETE].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_DELETE].name[1]);
+            }
+        break;
+    
+    case 'E':
+    case 'e':
+            if (imap_resps[RESP_EXISTS].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_EXISTS].name[1]);
+            }
+            else if (imap_resps[RESP_EXPUNGE].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_EXPUNGE].name[1]);
+            }
+
+        break;
+    
+    case 'R':
+    case 'r':
+            if (imap_resps[RESP_RENAME].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_RENAME].name[1]);
+            }
+        break;
+
+    case 'A':
+    case 'a':
+            if (imap_resps[RESP_APPEND].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_APPEND].name[1]);
+            }
+        break;
+
+    case 'B':
+    case 'b':
+            if (imap_resps[RESP_BAD].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_BAD].name[1]);
+            }
+            else if (imap_resps[RESP_BYE].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_BYE].name[1]);
+            }
+        break;
+
+    case 'O':
+    case 'o':
+        pfdata->imap_data_info.next_letter = &(imap_resps[RESP_OK].name[1]);
+        break;
+    
+    case 'U':
+    case 'u':
+            if (imap_resps[RESP_UID].name[1] == next_ch)
+            {
+                pfdata->imap_data_info.next_letter = &(imap_resps[RESP_UID].name[1]);
+            }
         break;
 
     default:
@@ -254,8 +411,21 @@ static inline void parse_command(const uint8_t ch, ImapPafData* pfdata)
     char val = *(pfdata->imap_data_info.next_letter);
 
     if (val == '\0' && isblank(ch))
-        pfdata->imap_state = IMAP_PAF_DATA_HEAD_STATE;
-
+    {
+        if (pfdata->imap_state == IMAP_PAF_CMD_STATUS)
+        {
+            pfdata->imap_state = IMAP_PAF_CMD_SEARCH;
+            pfdata->imap_data_info.next_letter = nullptr;
+            return;
+        }
+        
+        if (pfdata->imap_state == IMAP_PAF_CMD_SEARCH)
+        {
+            pfdata->imap_state = (pfdata->imap_data_info.cmd == imap_resps[RESP_FETCH].name) 
+                ? IMAP_PAF_DATA_HEAD_STATE 
+                : IMAP_PAF_VAL_STATE;
+        }
+    }
     else if (toupper(ch) == toupper(val))
         pfdata->imap_data_info.next_letter++;
 
@@ -272,57 +442,30 @@ static inline void parse_command(const uint8_t ch, ImapPafData* pfdata)
  *        const uint8_t ch - the next character to analyze.
  *        ImapPafData *pfdata - the struct containing all imap paf information
  */
-static inline void process_command(const uint8_t ch, ImapPafData* pfdata)
+static inline void process_command(const uint8_t ch, const uint8_t next_ch, ImapPafData* pfdata)
 {
     if (pfdata->imap_data_info.next_letter)
         parse_command(ch, pfdata);
     else
-        init_command_search(ch, pfdata);
+        init_command_search(ch, next_ch, pfdata);
 }
 
-/*
- * This function only does something when the character is a blank or a CR/LF.
- * In those specific cases, this function will set the appropriate next
- * state information
- *
- * PARAMS:
- *        const uint8_t ch - the next character to analyze.
- *        ImapPafData *pfdata - the struct containing all imap paf information
- *        ImapPafData base_state - if a space is not found, revert to this state
- *        ImapPafData next_state - if a space is found, go to this state
- * RETURNS:
- *        true - if the status has been eaten
- *        false - if a CR or LF has been found
- */
-static inline void eat_character(const uint8_t ch, ImapPafData* pfdata,
-    ImapPafState base_state, ImapPafState next_state)
+static inline void process_second_argument(const uint8_t ch, const uint8_t next_ch, ImapPafData* pfdata)
 {
-    switch (ch)
+    if (isdigit(ch))
+        return;
+    else if (isblank(ch))
     {
-    case ' ':
-    case '\t':
-        pfdata->imap_state = next_state;
-        break;
-
-    case '\r':
-    case '\n':
-        pfdata->imap_state = base_state;
-        break;
+        pfdata->imap_state = IMAP_PAF_CMD_SEARCH;
+        return;
     }
-}
 
-/*
- *  defined above in the eat_character function
- *
- * Keeping the next two functions to ease any future development
- * where these cases will no longer be simple or identical
- */
-static inline void eat_second_argument(const uint8_t ch, ImapPafData* pfdata)
-{
-    eat_character(ch, pfdata, IMAP_PAF_REG_STATE, IMAP_PAF_CMD_SEARCH);
+    if (pfdata->imap_data_info.next_letter)
+        parse_command(ch, pfdata);
+    else
+        init_command_search(ch, next_ch, pfdata);
 }
 
-/* explanation in 'eat_second_argument' above */
 static inline void eat_response_identifier(const uint8_t ch, ImapPafData* pfdata)
 {
     eat_character(ch, pfdata, IMAP_PAF_REG_STATE, IMAP_PAF_CMD_STATUS);
@@ -351,9 +494,14 @@ static StreamSplitter::Status imap_paf_server(ImapPafData* pfdata,
 
     pfdata->end_of_data = false;
 
+    bool saw_reg_state = false;
     for (i = 0; i < len; i++)
     {
         uint8_t ch = data[i];
+        uint8_t next_ch = 0;
+        if(i+1 < len)
+            next_ch = data[i+1];
+
         switch (pfdata->imap_state)
         {
         case IMAP_PAF_CMD_IDENTIFIER:
@@ -376,16 +524,23 @@ static StreamSplitter::Status imap_paf_server(ImapPafData* pfdata,
 
         case IMAP_PAF_CMD_STATUS:
             // can be a command name, msg sequence number, msg count, etc...
-            // since we are only interested in fetch, eat this argument
-            eat_second_argument(ch, pfdata);
+            process_second_argument(ch, next_ch, pfdata);
+            find_data_end_single_line(ch, pfdata);
+
             break;
 
         case IMAP_PAF_CMD_SEARCH:
-            process_command(ch, pfdata);
+            process_command(ch, next_ch, pfdata);
             find_data_end_single_line(ch, pfdata);
             break;
+        
+        case IMAP_PAF_VAL_STATE:
+            pfdata->server_bytes_seen = 0;
+            reset_data_states(pfdata);
+            return StreamSplitter::SEARCH;
 
         case IMAP_PAF_REG_STATE:
+            saw_reg_state = true;
             find_data_end_single_line(ch, pfdata); // data reset when end of line hit
             break;
 
@@ -424,6 +579,19 @@ static StreamSplitter::Status imap_paf_server(ImapPafData* pfdata,
         }
     }
 
+    if( saw_reg_state )
+    {
+        pfdata->server_bytes_seen += len;
+        if (pfdata->server_bytes_seen > IMAP_MAX_OCTETS)
+        {
+            return StreamSplitter::ABORT;
+        }
+    }
+    else 
+    {
+        pfdata->server_bytes_seen = 0;
+    }
+
     if (flush_len)
     {
         // flush at the final termination sequence
@@ -451,15 +619,96 @@ static StreamSplitter::Status imap_paf_server(ImapPafData* pfdata,
  *    StreamSplitter::Status - StreamSplitter::FLUSH if flush point found,
  *    StreamSplitter::SEARCH otherwise
  */
-static StreamSplitter::Status imap_paf_client(const uint8_t* data, uint32_t len, uint32_t* fp)
+
+static StreamSplitter::Status imap_paf_client(ImapPafData* pfdata, const uint8_t* data, uint32_t len, uint32_t* fp)
 {
-    const char* pch;
+    uint32_t i;
+    uint32_t flush_len = 0;
+    ImapClientState client_state = IMAP_CLIENT_FIRST_CHAR;
+    bool is_valid_command = false;
+    bool is_valid_base64 = len >= MIN_BASE64_LEN;
+    bool found_space = false;
+    
+    for (i = 0; i < len; i++)
+    {
+        uint8_t ch = data[i];
+        
+        if (ch == '\n')
+        {
+            flush_len = i + 1;
+            break;
+        }
+        else if (ch == '\r')
+        {
+            continue;
+        }
 
-    pch = (const char *)memchr (data, '\n', len);
+        switch (client_state)
+        {
+        case IMAP_CLIENT_FIRST_CHAR:
+            if (isalnum(ch))
+            {
+                is_valid_command = true;
+                client_state = IMAP_CLIENT_COMMAND_TAG;
+            }
+            else
+            {
+                is_valid_command = false;
+                client_state = is_valid_base64 ? IMAP_CLIENT_BASE64_CHECK : IMAP_CLIENT_FLUSH_LINE;
+            }
+            break;
 
-    if (pch != nullptr)
+        case IMAP_CLIENT_COMMAND_TAG:
+            if (ch == ' ')
+            {
+                found_space = true;
+                client_state = IMAP_CLIENT_FLUSH_LINE;
+            }
+            else if (!isalnum(ch) && ch != '-')
+            {
+                is_valid_command = false;
+                client_state = is_valid_base64 ? IMAP_CLIENT_BASE64_CHECK : IMAP_CLIENT_FLUSH_LINE;
+            }
+            break;
+
+        case IMAP_CLIENT_BASE64_CHECK:
+            if (i < MIN_BASE64_LEN)
+            {
+                if (!(isalnum(ch) || ch == '+' || ch == '/' || ch == '='))
+                {
+                    is_valid_base64 = false;
+                    client_state = IMAP_CLIENT_FLUSH_LINE;
+                }
+            }
+            break;
+
+        case IMAP_CLIENT_FLUSH_LINE:
+            break;
+        }
+    }
+
+    is_valid_command = is_valid_command && found_space;
+    bool is_valid = is_valid_command || is_valid_base64;
+    uint32_t check_len = flush_len ? flush_len : len;
+
+    if (is_valid)
+    {
+        pfdata->client_bytes_seen = 0;
+    }
+    else
     {
-        *fp = (uint32_t)(pch - (const char*)data) + 1;
+        pfdata->client_bytes_seen += check_len;
+        
+        if (pfdata->client_bytes_seen > IMAP_MAX_OCTETS)
+        {
+            pfdata->client_bytes_seen = 0;
+            return StreamSplitter::ABORT;
+        }
+    }
+    
+    if (flush_len)
+    {
+        *fp = flush_len;
         return StreamSplitter::FLUSH;
     }
     return StreamSplitter::SEARCH;
@@ -505,7 +754,7 @@ StreamSplitter::Status ImapSplitter::scan(
     if (flags & PKT_FROM_SERVER)
         return imap_paf_server(pfdata, data, len, fp);
     else
-        return imap_paf_client(data, len, fp);
+        return imap_paf_client(pfdata, data, len, fp);
 }
 
 bool imap_is_data_end(Flow* ssn)
index b98db903624125857e17d94faec9c29d32349c00..5c63e7e064b8939dc9f6bec33b9c79dde8c7bdf0 100644 (file)
@@ -27,6 +27,9 @@
 #include "mime/file_mime_paf.h"
 #include "stream/stream_splitter.h"
 
+#define IMAP_MAX_OCTETS 5120
+#define MIN_BASE64_LEN 4
+
 struct ImapDataInfo
 {
     int paren_cnt;            // The open parentheses count in fetch
@@ -34,12 +37,15 @@ struct ImapDataInfo
     bool found_len;
     uint32_t length;
     bool esc_nxt_char;        // true if the next character has been escaped
+    const char* cmd;
+
 };
 
-// States for IMAP PAF
+// States for IMAP PAF (Server)
 typedef enum _ImapPafState
 {
     IMAP_PAF_REG_STATE,           // default state. eat until LF
+    IMAP_PAF_VAL_STATE,           // parse a valid command 
     IMAP_PAF_DATA_HEAD_STATE,     // parses the fetch header
     IMAP_PAF_DATA_LEN_STATE,      // parse the literal length
     IMAP_PAF_DATA_STATE,          // search for and flush on MIME boundaries
@@ -50,6 +56,16 @@ typedef enum _ImapPafState
     IMAP_PAF_CMD_SEARCH           // currently searching data for a command
 } ImapPafState;
 
+// States for IMAP PAF (Client)
+typedef enum _ImapClientState
+{
+    IMAP_CLIENT_FIRST_CHAR,       // analyzing first character
+    IMAP_CLIENT_COMMAND_TAG,      // parsing command tag/name
+    IMAP_CLIENT_BASE64_CHECK,     // checking for base64-like data
+    IMAP_CLIENT_FLUSH_LINE        // ready to flush at newline
+} ImapClientState;
+
+
 typedef enum _ImapDataEnd
 {
     IMAP_PAF_DATA_END_UNKNOWN,
@@ -64,6 +80,8 @@ struct ImapPafData
     ImapDataInfo imap_data_info;  // Used for parsing data
     ImapDataEnd data_end_state;
     bool end_of_data;
+    uint32_t server_bytes_seen;
+    uint32_t client_bytes_seen;
 };
 
 class ImapSplitter : public snort::StreamSplitter