]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #4765: http_inspect: add support for partial_depth configuration option
authorAdrian Mamolea (admamole) <admamole@cisco.com>
Wed, 25 Jun 2025 17:46:35 +0000 (17:46 +0000)
committerRayen Mohanty (ramohant) <ramohant@cisco.com>
Wed, 25 Jun 2025 17:46:35 +0000 (17:46 +0000)
Merge in SNORT/snort3 from ~ADMAMOLE/snort3:cl to master

Squashed commit of the following:

commit 3e9cdd52035184e38416581e4d5ffb6fd4df0bd1
Author: Adrian Mamolea <admamole@cisco.com>
Date:   Fri May 23 15:48:16 2025 -0400

    http_inspect: add support for partial_depth configuration option

13 files changed:
doc/reference/builtin_stubs.txt
doc/user/http_inspect.txt
src/service_inspectors/http_inspect/dev_notes.txt
src/service_inspectors/http_inspect/dev_notes_partial_inspection.txt
src/service_inspectors/http_inspect/http_common.h
src/service_inspectors/http_inspect/http_enum.h
src/service_inspectors/http_inspect/http_flow_data.h
src/service_inspectors/http_inspect/http_inspect.cc
src/service_inspectors/http_inspect/http_module.cc
src/service_inspectors/http_inspect/http_module.h
src/service_inspectors/http_inspect/http_stream_splitter_reassemble.cc
src/service_inspectors/http_inspect/http_stream_splitter_scan.cc
src/service_inspectors/http_inspect/http_tables.cc

index eb8f21cd37263910c3f31b1696f5babfc5f7dee7..18fe8d842a91d1e31319d1f163ed05b379c25d0b 100644 (file)
@@ -1294,6 +1294,11 @@ HTTP request method is not on allowed methods list or is on disallowed methods l
 
 HTTP reserved GZIP flags are set
 
+119:289
+
+Too many partial flushes. Partial depth is enabled and 20 or more partial flushes are
+made before a regular flush.
+
 121:1
 
 Invalid flag set on HTTP/2 frame header
index 9dee396a2bd6273b7871f82dd08dfeb71d77e037..4557a13c39eba48a8291b5a33cbc34c0661d7f2f 100755 (executable)
@@ -148,6 +148,21 @@ more of the sensor's resources.
 
 This feature is off by default. script_detection = true will activate it.
 
+===== partial_depth
+
+Partial depth detection is a feature that enables Snort to more quickly detect
+and block malicious requests. It is configured by the partial_depth parameter
+which can take values in the range -1-16384 bytes. The feature is enabled by
+setting partial_depth to some non zero value. When the feature is enabled and
+either, the number of bytes received in the request body is below the value
+specified by partial_depth, or partial_depth is set to -1, unlimited; it
+immediately forwards the available part of the message body for early detection.
+This enables earlier threat detection but consumes somewhat more of the sensor's
+resources.
+
+This feature is turned off by default by setting partial_depth = 0. To activate
+it, set partial_depth to the desired value.
+
 ===== gzip
 
 http_inspect by default decompresses deflate and gzip message bodies
index 9d5a91a7b966eaad094e3e978d3a794f146b7408..d6f0c0035fc386af9cedcf28c5947e10b351825b 100755 (executable)
@@ -9,17 +9,7 @@ unexpectedly).
 The nature of splitting allows packets to be forwarded before they are aggregated into a message
 section and inspected. This may lead to problems when the target consumes a partial message
 body even though the end of the message body was never received because Snort blocked it.
-
-Script detection is a feature developed to solve this problem for message bodies containing
-Javascripts. The stream splitter scan() method searches its input for the end-of-script tag
-"</script>". When necessary this requires scan() to unzip the data. This is an extra unzip as
-storage limitations preclude saving the unzipped version of the data for subsequent reassembly.
-
-Update: the previous sentence has been discovered to be incorrect. The memory requirements of
-zlib are very large. It would save a lot of memory and some processing time for script detection
-to unzip one time in scan() and store the result for eventual use by reassemble(). The memory
-lost by storing partial message sections in HI while waiting for reassemble() would be more than
-compensated for by not having two instances of zlib.
+Partial inspection was developed to solve this problem.
 
 HttpFlowData is a data class representing all HI information relating to a flow. It serves as
 persistent memory between invocations of HI by the framework. It also glues together the inspector,
index 981643cb1dfacdf7482205794232e9092ce15b3a..ef70bbe0d06cfd9a6c6a12ad08fe8c7c0ad92be4 100644 (file)
@@ -1,16 +1,12 @@
-When the end of a script is found and the normal flush point has not been found, the current TCP
-segment and all previous segments for the current message section are flushed using a special
-procedure known as partial inspection. From the perspective of Stream (or H2I) a partial inspection
-is a regular flush in every respect.
-
-scan() calls prep_partial_flush() to prepare for the partial inspection. Then it returns a normal
-flush point to Stream at the end of the current TCP segment. Partial inspections perform all of the
-functions of a regular inspection including forwarding data to file processing and detection.
-
 The difference between a partial inspection and a regular inspection is reassemble() saves the
 input data for future reuse. Eventually there will be a regular full inspection of the entire
 message section. reassemble() will accomplish this by combining the input data for the partial
-inspection with later data that complete the message section.
+inspection with later data that completes the message section.
+
+scan() calls prep_partial_flush() to prepare for the partial inspection. Then it returns a normal
+flush point to Stream at the end of the current TCP segment. Partial inspections perform all of the
+functions of a regular inspection including forwarding data to file processing and detection. From
+the perspective of Stream (or H2I) a partial inspection is a regular flush in every respect.
 
 Correct and efficient execution of a full inspection following a partial inspection requires
 special handling of certain functions. Unzipping is only done once in reassemble(). The stored
@@ -26,12 +22,30 @@ Compared to just doing a full inspection, a partial inspection followed by a ful
 will not miss anything. The benefits of partial inspection are in addition to the benefits of a
 full inspection.
 
-The http_inspect partial inspection mechanism is also used by http2_inspect to manage frame
-boundaries. When inspecting HTTP/2, a partial inspection by http_inspect may occur because script
-detection triggered it, because H2I wanted it, or both. 
+Partial inspection is used in multiple scenarios, described below, and in combinations of them.
+
+Script detection uses partial inspection for message bodies containing Javascripts. The stream
+splitter scan() method searches its input for the end-of-script tag "</script>". When the end
+of a script is found and the normal flush point has not been found, the current TCP segment and
+all previous segments for the current message section are flushed using partial inspection.
+
+Searching for the end-of-script tag may require scan() to unzip the data. This is an extra unzip
+as storage limitations preclude saving the unzipped version of the data for subsequent reassembly.
+
+Update: the previous sentence has been discovered to be incorrect. The memory requirements of
+zlib are very large. It would save a lot of memory and some processing time for script detection
+to unzip one time in scan() and store the result for eventual use by reassemble(). The memory
+lost by storing partial message sections in HI while waiting for reassemble() would be more than
+compensated for by not having two instances of zlib.
+
+For request bodies, when partial_depth parameter is set to a non zero value, a partial body will
+be subjected to partial inspection if its length is below partial_depth value. When the partial_depth
+parameter is set to -1, the entire body will be subjected to inspection regardless of its length.
+
+The http_inspect partial inspection mechanism is invoked by http2_inspect on frame boundaries.
 
-Some applications may be affected by blocks too late scenarios related to seeing part of the
-zero-length chunk. For example a TCP packet that ends with:
+With chunking some applications may be affected by blocks too late scenarios related to seeing part
+of the zero-length chunk. For example a TCP packet that ends with:
 
     8<CR><LF>abcdefgh<CR><LF>0
 
index 4dac4bd996ba92af0e0c1b859e06d119edcb98d0..1d61cfbf6de45ee0da9e621db186ff2e3ac3eeee 100644 (file)
@@ -40,6 +40,11 @@ enum SectionType { SEC_DISCARD = -19, SEC_ABORT = -18, SEC__NOT_COMPUTE=-14, SEC
     SEC_REQUEST = 2, SEC_STATUS, SEC_HEADER, SEC_BODY_CL, SEC_BODY_CHUNK, SEC_TRAILER,
     SEC_BODY_OLD, SEC_BODY_HX };
 
+inline bool is_body(SectionType st)
+{
+    return (st == SEC_BODY_CL || st == SEC_BODY_CHUNK || st == SEC_BODY_OLD || st == SEC_BODY_HX);
+}
+
 // Caters to all extended versions of HTTP, i.e. HTTP/2, HTTP/3
 enum HXBodyState { HX_BODY_NOT_COMPLETE, HX_BODY_LAST_SEG, HX_BODY_COMPLETE,
     HX_BODY_COMPLETE_EXPECT_TRAILERS, HX_BODY_NO_BODY };
index 1c8984d9ea3154f20bd61715c5af96aaf51401a8..fbac0fa09655d1eb52a57e40ef121f2cd5c3dd4a 100755 (executable)
@@ -444,6 +444,7 @@ enum EventSid
     EVENT_UNEXPECTED_H2_PREFACE = 286,
     EVENT_DISALLOWED_METHOD = 287,
     EVENT_GZIP_RESERVED_FLAGS = 288,
+    EVENT_MAX_PARTIAL_FLUSH = 289,
     EVENT__MAX_VALUE
 };
 
index 39d774173151a3bcb62fab6d79e7ccf7f9902189..c31d054cbf5c804594d9658ddee65466d90df558 100644 (file)
@@ -120,6 +120,7 @@ private:
     uint32_t num_good_chunks[2] = { 0, 0 };
     uint32_t octets_expected[2] = { 0, 0 };
     bool is_broken_chunk[2] = { false, false };
+    uint64_t partial_flush_counter = 0;
 
     // *** StreamSplitter => Inspector (facts about the most recent message section)
     HttpCommon::SectionType section_type[2] = { HttpCommon::SEC__NOT_COMPUTE,
index d6f69576d221c1204ba8922e43352a2c6ec56b45..ad8da1c478971756d46213e7e674b4a482325544 100755 (executable)
@@ -158,6 +158,7 @@ void HttpInspect::show(const SnortConfig*) const
 
     ConfigLogger::log_limit("request_depth", params->request_depth, -1);
     ConfigLogger::log_limit("response_depth", params->response_depth, -1);
+    ConfigLogger::log_limit("partial_depth", params->partial_depth, -1, 0);
     ConfigLogger::log_flag("unzip", params->unzip);
     ConfigLogger::log_flag("normalize_utf", params->normalize_utf);
     ConfigLogger::log_flag("decompress_pdf", params->decompress_pdf);
index f11b8ff6b5501a8e66f13730cf29e28043df0a92..a9749d9fa9176ee72cd0c928492d39579456967b 100755 (executable)
@@ -52,6 +52,9 @@ const Parameter HttpModule::http_params[] =
     { "response_depth", Parameter::PT_INT, "-1:max53", "-1",
       "maximum response message body bytes to examine (-1 no limit)" },
 
+    { "partial_depth", Parameter::PT_INT, "-1:16384", "0",
+      "maximum request body to send to early detection (0 disabled, -1 no limit)" },
+
     { "unzip", Parameter::PT_BOOL, nullptr, "true",
       "decompress gzip and deflate message bodies" },
 
@@ -209,6 +212,10 @@ bool HttpModule::set(const char*, Value& val, SnortConfig*)
     {
         params->response_depth = val.get_int64();
     }
+    else if (val.is("partial_depth"))
+    {
+        params->partial_depth = val.get_int64();
+    }
     else if (val.is("unzip"))
     {
         params->unzip = val.get_bool();
index fe09762599b74c9c2016a260af408182ff44624c..c333a424e301de60d2a220de0f47b76b7ca8171c 100755 (executable)
@@ -49,6 +49,7 @@ public:
     ~HttpParaList();
     int64_t request_depth = -1;
     int64_t response_depth = -1;
+    int64_t partial_depth = 0;
 
     bool unzip = true;
     bool normalize_utf = true;
index a22f37fd046cb3f75c106de4eddf3ccbe36e964f..eb7c07a532d62f51daf721f758e04b1abfc020c9 100644 (file)
@@ -421,17 +421,11 @@ const StreamBuffer HttpStreamSplitter::reassemble(Flow* flow, unsigned total,
 
     HttpModule::increment_peg_counts(PEG_REASSEMBLE);
 
-    const bool is_body =
-        (session_data->section_type[source_id] == SEC_BODY_CHUNK) ||
-        (session_data->section_type[source_id] == SEC_BODY_CL) ||
-        (session_data->section_type[source_id] == SEC_BODY_OLD) ||
-        (session_data->section_type[source_id] == SEC_BODY_HX);
-
     uint8_t*& buffer = session_data->section_buffer[source_id];
     if (buffer == nullptr)
     {
         // Body sections need extra space to accommodate unzipping
-        if (is_body)
+        if (is_body(session_data->section_type[source_id]))
             buffer = new uint8_t[MAX_OCTETS];
         else
         {
index bbc227b3001bcfd2c564a8bc4f9d4583ffcbf5ee..9db07ecef3e226558695fc493847271e83674853 100644 (file)
@@ -190,7 +190,7 @@ StreamSplitter::Status HttpStreamSplitter::call_cutter(Flow* flow, HttpFlowData*
     }
 
     const uint32_t max_length = MAX_OCTETS - cutter->get_octets_seen();
-    const ScanResult cut_result = cutter->cut(data, (length <= max_length) ? length :
+    ScanResult cut_result = cutter->cut(data, (length <= max_length) ? length :
         max_length, session_data->get_infractions(source_id), session_data->events[source_id],
         session_data->section_size_target[source_id],
         session_data->stretch_section_to_packet[source_id],
@@ -217,6 +217,16 @@ StreamSplitter::Status HttpStreamSplitter::call_cutter(Flow* flow, HttpFlowData*
             return status_value(StreamSplitter::ABORT);
         }
 
+        if (is_body(type) && source_id == SRC_CLIENT &&
+            (my_inspector->params->partial_depth == -1 ||
+             (cutter->get_octets_seen() < my_inspector->params->partial_depth && cutter->get_num_flush() == 0)))
+        {
+            static const uint64_t MAX_PARTIAL_FLUSH_COUNTER = 20;
+            if (++session_data->partial_flush_counter == MAX_PARTIAL_FLUSH_COUNTER)
+                session_data->events[source_id]->create_event(HttpEnums::EVENT_MAX_PARTIAL_FLUSH);
+            cut_result = SCAN_NOT_FOUND_ACCELERATE;
+        }
+
         if (cut_result == SCAN_NOT_FOUND_ACCELERATE)
         {
             prep_partial_flush(flow, length);
@@ -252,6 +262,7 @@ StreamSplitter::Status HttpStreamSplitter::call_cutter(Flow* flow, HttpFlowData*
     case SCAN_FOUND:
     case SCAN_FOUND_PIECE:
       {
+        session_data->partial_flush_counter = 0;
         const uint32_t flush_octets = cutter->get_num_flush();
         prepare_flush(session_data, flush_offset, type, flush_octets, cutter->get_num_excess(),
             cutter->get_num_head_lines(), cutter->get_is_broken_chunk(),
index c8a963f5bc599e725ab26af61c2521108a3024b5..5b27de7c0912f98f35904364a74a09d642d2ab77 100755 (executable)
@@ -354,6 +354,7 @@ const RuleMap HttpModule::http_events[] =
     { EVENT_DISALLOWED_METHOD,          "HTTP request method is not on allowed methods list or is on "
                                         "disallowed methods list" },
     { EVENT_GZIP_RESERVED_FLAGS,        "HTTP gzip body with reserved flag set" },
+    { EVENT_MAX_PARTIAL_FLUSH,          "Too many partial flushes" },
     { 0, nullptr }
 };