]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #5181: payload_injector: add support for payload injection on s2c packet...
authorAdrian Mamolea (admamole) <admamole@cisco.com>
Tue, 3 Mar 2026 18:52:32 +0000 (18:52 +0000)
committerPriyanka Gurudev (prbg) <prbg@cisco.com>
Tue, 3 Mar 2026 18:52:32 +0000 (18:52 +0000)
Merge in SNORT/snort3 from ~ADMAMOLE/snort3:test_s2c to master

Squashed commit of the following:

commit 5f0f358b3c2864c2a11d9697c8ce5046c2dfa7b6
Author: Adrian Mamolea <admamole@cisco.com>
Date:   Thu Feb 26 13:58:35 2026 -0500

    payload_injector: add support for payload injection on s2c packets for http and http2 traffic

27 files changed:
doc/user/active.txt
src/actions/act_react.cc
src/actions/dev_notes.txt
src/detection/event_trace.cc
src/framework/base_api.h
src/payload_injector/dev_notes.txt
src/payload_injector/payload_injector.cc
src/payload_injector/payload_injector.h
src/payload_injector/payload_injector_module.cc
src/payload_injector/payload_injector_module.h
src/payload_injector/test/payload_injector_test.cc
src/protocols/packet.h
src/pub_sub/test/pub_sub_http_transaction_end_event_test.cc
src/service_inspectors/http2_inspect/dev_notes.txt
src/service_inspectors/http2_inspect/http2_enum.h
src/service_inspectors/http2_inspect/http2_flow_data.h
src/service_inspectors/http2_inspect/http2_stream_splitter.cc
src/service_inspectors/http2_inspect/http2_stream_splitter.h
src/service_inspectors/http2_inspect/http2_stream_splitter_impl.cc
src/service_inspectors/http_inspect/dev_notes.txt
src/service_inspectors/http_inspect/dev_notes_block_page_injection.txt [new file with mode: 0644]
src/service_inspectors/http_inspect/http_stream_splitter.h
src/service_inspectors/http_inspect/http_stream_splitter_base.h
src/service_inspectors/http_inspect/http_stream_splitter_scan.cc
src/service_inspectors/http_inspect/test/http_transaction_test.cc
src/stream/tcp/tcp_reassembly_segments.cc
src/stream/tcp/tcp_segment_descriptor.h

index 3061240ef65e7a6817ec157f09327d1ba2fd81d5..c3602a51ecbfa3e2190f8e3dfa8a86dd4aa4f76c 100644 (file)
@@ -122,7 +122,7 @@ translate the page to HTTP/2 format.
 
 Limitations for HTTP/2:
 
-* Packet will be injected against the last received stream id.
+* Packet will be injected against the last processed stream id.
 
 * Injection triggered while server-to-client flow of traffic is in a middle
 of a frame is not supported. The traffic will be blocked, but the page will
index a3c20ac510930c06343f378e988bcde3f25e188e..1daa1cb3b7905612e843290acf2f6c6ae5ab0cfe 100644 (file)
@@ -180,7 +180,7 @@ void ReactActiveAction::send(Packet* p)
     if (p->flow && p->flow->gadget &&
         (strcmp(p->flow->gadget->get_name(), "http2_inspect") == 0))
     {
-        Http2FlowData* const session_data =
+        const Http2FlowData* const session_data =
             (Http2FlowData*)p->flow->get_flow_data(Http2FlowData::inspector_id);
         assert(session_data != nullptr);
         const SourceId source_id = p->is_from_client() ? SRC_CLIENT : SRC_SERVER;
@@ -227,6 +227,9 @@ void ReactAction::exec(Packet* p, const ActInfo& ai)
     p->active->drop_packet(p);
     p->active->set_drop_reason("ips");
 
+    if ( p->flow )
+        p->flow->disable_inspection();
+
     if ( p->context->wire_packet and
          !p->active->is_reset_candidate(p->context->wire_packet) )
     {
index 97212d39e719c3a9571b25969fbb91e3dfeefee9..fc7c431436b11eb94c01f45d83bef54060b4d7e5 100644 (file)
@@ -15,7 +15,11 @@ TCP resets (TCP connections) or ICMP unreachable packets.
 
 React sends an HTML page to the client, a RST to the server and blocks the flow.
 It is using payload_injector utility. payload_injector should be configured when
-react is used.
+react is used. In some cases, block page injection is not possible. For example,
+if the client is expecting a response body rather than a status line, injecting
+a block page would be invalid. For more details look at implementation of
+payload_injector. In either case, even when injection of the block page is
+not supported, react blocks the flow.
 
 Rewrite enables overwrite packet contents based on "replace" option in the
 rules. Note that using "rewrite" action without "replace" option will raise
index 4fd7c81293c130f92ba8af259d205321fb08e68a..a96465f44fa6cf3ee1e2e6ae5dfc77fdacbe3947 100644 (file)
@@ -92,7 +92,7 @@ void EventTrace_Log(const Packet* p, const OptTreeNode* otn, IpsAction::Type act
         p->pkth->pktlen, p->pktlen);
 
     TextLog_Print(tlog,
-        "Pkt Bits: Flags=0x%X, Proto=0x%X, Err=0x%X\n",
+        "Pkt Bits: Flags=0x" STDx64 ", Proto=0x%X, Err=0x%X\n",
         p->packet_flags, (unsigned)p->proto_bits, (unsigned)p->ptrs.decode_flags);
 
     TextLog_Print(tlog,
@@ -134,4 +134,3 @@ void EventTrace_Term()
         TextLog_Term(tlog);
     }
 }
-
index a0307995f89740437171250ed198c3eb3a26fd43..713f283df3cbfe927f35625c6da5a13f1ead1a56 100644 (file)
@@ -38,7 +38,7 @@
 // depends on includes installed in framework/snort_api.h
 // see framework/plugins.h
 
-#define BASE_API_VERSION 24
+#define BASE_API_VERSION 25
 
 #define PLUGIN_DEFAULT    0x0
 #define PLUGIN_SO_RELOAD  0x1  // assumed for PT_SO_RULE
index 99500cff78ecc7493f22e221b08a30669260c28d..a204fdeafcbe413db162b6aa02907c4db7906d34 100644 (file)
@@ -9,6 +9,17 @@ injection triggered while server-to-client flow of traffic is in a middle of a
 frame is not supported. The traffic will be blocked, but the page will not be
 injected/displayed.
 
+There are other cases when payload_injector doesn't inject a block page:
+* payload_injector is not configured
+* packet doesn't have a flow or the flow's session is not established
+* packet is not TCP or stream_tcp marked the packet as one for which a block page
+  cannot be injected
+* there is conflicting s2c traffic such as queued client segments
+* http_inspect or http2_inspect marked the packet as one for which a block page
+  cannot be injected
+* the service inspector for the flow is neither http_inspect nor http2_inspect
+* for HTTP/2, the target stream ID is zero or even (server-initiated)
+
 get_http2_payload supports translation of HTTP block/redirect page to HTTP2.
 Current implementation is limited, the constraints are specified in
 payload_injector_translate_page.cc.
index 91b5ed809700e2bcea366de34289121da5f534cf..0cb1f152e3f7e4c598d8bde9d11a4a5adf40da9d 100644 (file)
@@ -50,8 +50,13 @@ static const std::map <InjectionReturnStatus, const char*> InjectionErrorToStrin
     { ERR_TRANSLATED_HDRS_SIZE,
       "HTTP/2 translated header size is bigger than expected. Update max size." },
     { ERR_HTTP2_EVEN_STREAM_ID, "HTTP/2 - injection to server initiated stream" },
-    { ERR_PKT_FROM_SERVER, "Packet is from server" },
-    { ERR_CONFLICTING_S2C_TRAFFIC, "Conflicting S2C HTTP traffic in progress" }
+    { ERR_CONFLICTING_S2C_TRAFFIC, "Conflicting S2C HTTP traffic in progress" },
+    { ERR_SESSION_NOT_TCP, "not a TCP stream" },
+    { ERR_STALE_S2C_DATA, "S2C injection blocked: packet fills hole with pending out-of-order, "
+      "retransmitted, or overlapping segments" },
+    { ERR_S2C_HTTP_PROTO, "HTTP/1 injection blocked on server response due to protocol state conflict" },
+    { ERR_C2S_HTTP_PROTO, "HTTP/1 injection blocked on client request due to protocol state conflict" },
+    { ERR_S2C_HTTP2_PROTO, "HTTP/2 injection blocked on server response due to protocol state conflict" }
 };
 
 InjectionReturnStatus PayloadInjector::inject_http2_payload(Packet* p,
@@ -60,10 +65,14 @@ InjectionReturnStatus PayloadInjector::inject_http2_payload(Packet* p,
     InjectionReturnStatus status;
 
     if (control.stream_id == 0)
+    {
+        payload_injector_stats.err_http2_stream_id_0++;
         status = ERR_HTTP2_STREAM_ID_0;
+    }
     else if (control.stream_id % 2 == 0)
     {
         // Don't inject against server initiated streams
+        payload_injector_stats.err_http2_even_stream++;
         status = ERR_HTTP2_EVEN_STREAM_ID;
     }
     else
@@ -77,11 +86,18 @@ InjectionReturnStatus PayloadInjector::inject_http2_payload(Packet* p,
             // FIXIT-E mid-frame injection not supported
             status = ERR_HTTP2_MID_FRAME;
         }
-        else if (p->flow->session and
+        else if (p->flow->session and p->packet_flags & PKT_FROM_CLIENT and
             p->flow->session->are_client_segments_queued())
         {
+            payload_injector_stats.err_conflicting_s2c_traffic++;
             status = ERR_CONFLICTING_S2C_TRAFFIC;
         }
+        else if ((p->packet_flags & PKT_FROM_SERVER)
+                && (p->packet_flags & PKT_HTTP_INJECT_BLOCKED))
+        {
+            payload_injector_stats.err_s2c_http2_proto++;
+            status = ERR_S2C_HTTP2_PROTO;
+        }
         else
         {
             uint8_t* http2_payload;
@@ -113,44 +129,82 @@ InjectionReturnStatus PayloadInjector::inject_http_payload(Packet* p,
     assert(p != nullptr);
 
     const PayloadInjectorConfig* conf = p->context->conf->payload_injector_config;
-    if (conf)
+
+    if (!conf)
     {
+        payload_injector_stats.err_injector_not_configured++;
+        status = ERR_INJECTOR_NOT_CONFIGURED;
+    }
+    else if (!p->flow)
+    {
+        payload_injector_stats.err_unidentified_protocol++;
+        status = ERR_UNIDENTIFIED_PROTOCOL;
+    }
+    else if (!(p->flow->ssn_state.session_flags & SSNFLAG_ESTABLISHED))
+    {
+        payload_injector_stats.err_stream_not_established++;
+        status = ERR_STREAM_NOT_ESTABLISHED;
+    }
+    else if (p->flow->pkt_type != PktType::TCP)
+    {
+        payload_injector_stats.err_session_not_tcp++;
+        status = ERR_SESSION_NOT_TCP;
+    }
+    else
+    {
+        EncodeFlags df = ENC_FLAG_RST_SRVR; // Send RST to server.
+
         if (p->packet_flags & PKT_FROM_SERVER)
-            status =  ERR_PKT_FROM_SERVER;
-        else
-        {
-            EncodeFlags df = ENC_FLAG_RST_SRVR; // Send RST to server.
+            df |= ENC_FLAG_FWD;
 
-            if (!p->flow)
-                status = ERR_UNIDENTIFIED_PROTOCOL;
-            else if (p->flow->ssn_state.session_flags & SSNFLAG_ESTABLISHED)
+        if ((p->packet_flags & PKT_FROM_SERVER)
+             && (p->packet_flags & PKT_TCP_INJECT_BLOCKED))
+        {
+            payload_injector_stats.err_stale_s2c_data++;
+            status = ERR_STALE_S2C_DATA;
+            p->active->send_data(p, df, nullptr, 0);    // To send reset
+        }
+        // FIXIT-M should we be supporting injection when there is no gadget on the flow?
+        else if (!p->flow->gadget || strcmp(p->flow->gadget->get_name(), "http_inspect") == 0)
+        {
+            if (p->flow->session and p->packet_flags & PKT_FROM_CLIENT and
+                p->flow->session->are_client_segments_queued())
             {
-                // FIXIT-M should we be supporting injection when there is no gadget on the flow?
-                if (!p->flow->gadget || strcmp(p->flow->gadget->get_name(), "http_inspect") == 0)
+                payload_injector_stats.err_conflicting_s2c_traffic++;
+                status = ERR_CONFLICTING_S2C_TRAFFIC;
+                p->active->send_data(p, df, nullptr, 0);    // To send reset
+            }
+            else if (p->packet_flags & PKT_HTTP_INJECT_BLOCKED)
+            {
+                if (p->packet_flags & PKT_FROM_SERVER)
                 {
-                    if (p->flow->session and
-                        p->flow->session->are_client_segments_queued())
-                    {
-                        status = ERR_CONFLICTING_S2C_TRAFFIC;
-                        p->active->send_data(p, df, nullptr, 0);    // To send reset
-                    }
-                    else
-                    {
-                        payload_injector_stats.http_injects++;
-                        p->active->send_data(p, df, control.http_page, control.http_page_len);
-                    }
+                    payload_injector_stats.err_s2c_http_proto++;
+                    status = ERR_S2C_HTTP_PROTO;
                 }
-                else if (strcmp(p->flow->gadget->get_name(),"http2_inspect") == 0)
-                    status = inject_http2_payload(p, control, df);
                 else
-                    status = ERR_UNIDENTIFIED_PROTOCOL;
+                {
+                    payload_injector_stats.err_c2s_http_proto++;
+                    status = ERR_C2S_HTTP_PROTO;
+                }
+                p->active->send_data(p, df, nullptr, 0);    // To send reset
             }
             else
-                status = ERR_STREAM_NOT_ESTABLISHED;
+            {
+                payload_injector_stats.http_injects++;
+                p->active->send_data(p, df, control.http_page, control.http_page_len);
+            }
+        }
+        else if (strcmp(p->flow->gadget->get_name(),"http2_inspect") == 0)
+            status = inject_http2_payload(p, control, df);
+        else
+        {
+            payload_injector_stats.err_unidentified_protocol++;
+            status = ERR_UNIDENTIFIED_PROTOCOL;
         }
     }
-    else
-        status = ERR_INJECTOR_NOT_CONFIGURED;
+
+    if (status != INJECTION_SUCCESS)
+        payload_injector_stats.failed_injects++;
 
     p->active->block_session(p, true);
 
index f2ead41aac631fcbd61e7669ac5f6f7e5e88e1d7..5bb43a7aaad98e07dcd9b35bba5e3b2d00294a07 100644 (file)
@@ -39,8 +39,12 @@ enum InjectionReturnStatus : int8_t
     ERR_HTTP2_MID_FRAME = -6,
     ERR_TRANSLATED_HDRS_SIZE = -7,
     ERR_HTTP2_EVEN_STREAM_ID = -8,
-    ERR_PKT_FROM_SERVER = -9,
-    ERR_CONFLICTING_S2C_TRAFFIC = -10,
+    ERR_CONFLICTING_S2C_TRAFFIC = -9,
+    ERR_SESSION_NOT_TCP = -10,
+    ERR_STALE_S2C_DATA = -11,
+    ERR_S2C_HTTP_PROTO= -12,
+    ERR_C2S_HTTP_PROTO= -13,
+    ERR_S2C_HTTP2_PROTO= -14
     // Update InjectionErrorToString when adding/removing error codes
 };
 
index 914723c3ed1609e89accb3d18a36359eda42e1c2..9af17199d5f6c275ee260e24412f0534e33a5b53 100644 (file)
@@ -38,10 +38,22 @@ THREAD_LOCAL PayloadInjectorCounts payload_injector_stats;
 
 const PegInfo payload_injector_pegs[] =
 {
-    { CountType::SUM, "http_injects", "total number of http injections" },
-    { CountType::SUM, "http2_injects", "total number of http2 injections" },
-    { CountType::SUM, "http2_translate_err", "total number of http2 page translation errors" },
+    { CountType::SUM, "http_injects", "total number of HTTP injections" },
+    { CountType::SUM, "http2_injects", "total number of HTTP/2 injections" },
+    { CountType::SUM, "failed_injects", "total number of failed HTTP and HTTP/2 injections" },
+    { CountType::SUM, "http2_translate_err", "total number of HTTP/2 page translation errors" },
     { CountType::SUM, "http2_mid_frame", "total number of attempts to inject mid-frame" },
+    { CountType::SUM, "err_unidentified_protocol", "total number of unidentified-protocol errors" },
+    { CountType::SUM, "err_stream_not_established", "total number of stream-not-established errors" },
+    { CountType::SUM, "err_injector_not_configured", "total number of injector-not-configured errors" },
+    { CountType::SUM, "err_conflicting_s2c_traffic", "total number of conflicting s2c traffic errors" },
+    { CountType::SUM, "err_http2_even_stream", "total number of HTTP/2 even-numbered stream errors" },
+    { CountType::SUM, "err_http2_stream_id_0", "total number of HTTP/2 stream ID 0 errors" },
+    { CountType::SUM, "err_session_not_tcp", "total number of session-not-tcp errors" },
+    { CountType::SUM, "err_stale_s2c_data", "total number of stale s2c data errors" },
+    { CountType::SUM, "err_s2c_http_proto", "total number of s2c HTTP protocol errors" },
+    { CountType::SUM, "err_c2s_http_proto", "total number of c2s HTTP protocol errors" },
+    { CountType::SUM, "err_s2c_http2_proto", "total number of s2c HTTP2 protocol errors" },
     { CountType::END, nullptr, nullptr }
 };
 
index a9730676b7f6731dabca589c7c1cb00954185e24..63946f5242405d3f024645844260801ec79dac38 100644 (file)
@@ -27,8 +27,20 @@ struct PayloadInjectorCounts
 {
     PegCount http_injects;
     PegCount http2_injects;
+    PegCount failed_injects;
     PegCount http2_translate_err;
     PegCount http2_mid_frame;
+    PegCount err_unidentified_protocol;
+    PegCount err_stream_not_established;
+    PegCount err_injector_not_configured;
+    PegCount err_conflicting_s2c_traffic;
+    PegCount err_http2_even_stream;
+    PegCount err_http2_stream_id_0;
+    PegCount err_session_not_tcp;
+    PegCount err_stale_s2c_data;
+    PegCount err_s2c_http_proto;
+    PegCount err_c2s_http_proto;
+    PegCount err_s2c_http2_proto;
 };
 
 extern THREAD_LOCAL PayloadInjectorCounts payload_injector_stats;
index d24f9916711dcce7f19992f012be282dedb3192e..6fef68d37a3292d7afbffb08ccdc2fc07066885c 100644 (file)
@@ -28,6 +28,7 @@
 
 #include "detection/detection_engine.h"
 #include "flow/flow.h"
+#include "flow/session.h"
 #include "main/snort_config.h"
 #include "main/thread_config.h"
 #include "packet_io/active.h"
@@ -119,6 +120,18 @@ public:
     bool configure(snort::SnortConfig*) override { return true; }
 };
 
+class MockSession : public Session
+{
+public:
+    explicit MockSession(snort::Flow* f, bool has_queued) : Session(f), queued(has_queued) { }
+
+    void clear() override { }
+    bool are_client_segments_queued() const override { return queued; }
+
+private:
+    bool queued;
+};
+
 // Mocks for PayloadInjectorModule::get_http2_payload
 
 static InjectionReturnStatus translation_status = INJECTION_SUCCESS;
@@ -171,12 +184,26 @@ TEST_GROUP(payload_injector_test)
     {
         counts->http_injects = 0;
         counts->http2_injects = 0;
+        counts->failed_injects = 0;
         counts->http2_translate_err = 0;
         counts->http2_mid_frame = 0;
+        counts->err_unidentified_protocol = 0;
+        counts->err_stream_not_established = 0;
+        counts->err_injector_not_configured = 0;
+        counts->err_conflicting_s2c_traffic = 0;
+        counts->err_http2_even_stream = 0;
+        counts->err_http2_stream_id_0 = 0;
+        counts->err_session_not_tcp = 0;
+        counts->err_stale_s2c_data = 0;
+        counts->err_s2c_http_proto = 0;
+        counts->err_c2s_http_proto = 0;
+        counts->err_s2c_http2_proto = 0;
         control.http_page = (const uint8_t*)"test";
         control.http_page_len = 4;
+        control.stream_id = 0;
         flow.set_state(Flow::FlowState::INSPECT);
         flow.set_session_flags(SSNFLAG_ESTABLISHED);
+        flow.pkt_type = PktType::TCP;
         translation_status = INJECTION_SUCCESS;
         http2_flow_data.set_mid_frame(false);
     }
@@ -189,10 +216,11 @@ TEST(payload_injector_test, not_configured_stream_not_established)
     p.flow = &flow;
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
     CHECK(counts->http_injects == 0);
+    CHECK(counts->err_injector_not_configured == 1);
     CHECK(status == ERR_INJECTOR_NOT_CONFIGURED);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
     const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string, "Payload injector is not configured") == 0);
+    STRCMP_EQUAL("Payload injector is not configured", err_string);
 }
 
 TEST(payload_injector_test, not_configured_stream_established)
@@ -202,6 +230,8 @@ TEST(payload_injector_test, not_configured_stream_established)
     p.flow = &flow;
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
     CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_injector_not_configured == 1);
     CHECK(status == ERR_INJECTOR_NOT_CONFIGURED);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
 }
@@ -214,9 +244,11 @@ TEST(payload_injector_test, configured_stream_not_established)
     flow.update_session_flags(0);
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
     CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_stream_not_established == 1);
     CHECK(status == ERR_STREAM_NOT_ESTABLISHED);
     const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string, "TCP stream not established") == 0);
+    STRCMP_EQUAL("TCP stream not established", err_string);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
 }
 
@@ -244,10 +276,12 @@ TEST(payload_injector_test, http2_stream0)
     p.flow = &flow;
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
     CHECK(counts->http2_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_http2_stream_id_0 == 1);
     CHECK(status == ERR_HTTP2_STREAM_ID_0);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
     const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string, "HTTP/2 - injection to stream 0") == 0);
+    STRCMP_EQUAL("HTTP/2 - injection to stream 0", err_string);
     delete flow.gadget;
 }
 
@@ -261,10 +295,12 @@ TEST(payload_injector_test, http2_even_stream_id)
     control.stream_id = 2;
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
     CHECK(counts->http2_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_http2_even_stream == 1);
     CHECK(status == ERR_HTTP2_EVEN_STREAM_ID);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
     const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string, "HTTP/2 - injection to server initiated stream") == 0);
+    STRCMP_EQUAL("HTTP/2 - injection to server initiated stream", err_string);
     delete flow.gadget;
 }
 
@@ -304,6 +340,9 @@ TEST(payload_injector_test, unidentified_gadget_name)
     flow.gadget = new MockInspector();
     p.flow = &flow;
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
+    CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_unidentified_protocol == 1);
     CHECK(status == ERR_UNIDENTIFIED_PROTOCOL);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
     delete flow.gadget;
@@ -319,53 +358,180 @@ TEST(payload_injector_test, http2_mid_frame)
     control.stream_id = 1;
     http2_flow_data.set_mid_frame(true);
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
+    CHECK(counts->http2_injects == 0);
+    CHECK(counts->failed_injects == 1);
     CHECK(counts->http2_mid_frame == 1);
     CHECK(status == ERR_HTTP2_MID_FRAME);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
     const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string, "HTTP/2 - attempt to inject mid frame. Currently not supported.")
-        == 0);
+    STRCMP_EQUAL("HTTP/2 - attempt to inject mid frame. Currently not supported.",
+        err_string);
     delete flow.gadget;
 }
 
-TEST(payload_injector_test, http2_continuation_expected)
+TEST(payload_injector_test, http_pkt_from_srvr)
 {
     Packet p(false);
     set_configured();
-    mock_api.base.name = "http2_inspect";
+    mock_api.base.name = "http_inspect";
+    p.packet_flags = PKT_FROM_SERVER;
     flow.gadget = new MockInspector();
     p.flow = &flow;
-    control.stream_id = 1;
-    http2_flow_data.set_mid_frame(true);
+    p.active = &active;
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
-    CHECK(counts->http2_mid_frame == 1);
-    CHECK(status == ERR_HTTP2_MID_FRAME);
+    CHECK(counts->http_injects == 1);
+    CHECK(status == INJECTION_SUCCESS);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
     delete flow.gadget;
 }
 
-TEST(payload_injector_test, http2_pkt_from_srvr)
+TEST(payload_injector_test, flow_is_null)
 {
     Packet p(false);
     set_configured();
-    p.packet_flags = PKT_FROM_SERVER;
+    InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
+    CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_unidentified_protocol == 1);
+    CHECK(status == ERR_UNIDENTIFIED_PROTOCOL);
+    const char* err_string = PayloadInjector::get_err_string(status);
+    STRCMP_EQUAL("Unidentified protocol", err_string);
+}
+
+TEST(payload_injector_test, session_not_tcp)
+{
+    Packet p(false);
+    set_configured();
+    p.flow = &flow;
+    p.active = &active;
+    flow.pkt_type = PktType::UDP;
+    InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
+    CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_session_not_tcp == 1);
+    CHECK(status == ERR_SESSION_NOT_TCP);
+    CHECK(flow.flow_state == Flow::FlowState::BLOCK);
+    const char* err_string = PayloadInjector::get_err_string(status);
+    STRCMP_EQUAL("not a TCP stream", err_string);
+}
+
+TEST(payload_injector_test, stale_s2c_data)
+{
+    Packet p(false);
+    set_configured();
+    p.flow = &flow;
+    p.active = &active;
+    p.packet_flags = PKT_FROM_SERVER | PKT_TCP_INJECT_BLOCKED;
+    InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
+    CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_stale_s2c_data == 1);
+    CHECK(status == ERR_STALE_S2C_DATA);
+    CHECK(flow.flow_state == Flow::FlowState::BLOCK);
+    const char* err_string = PayloadInjector::get_err_string(status);
+    STRCMP_EQUAL("S2C injection blocked: packet fills hole with pending out-of-order, "
+        "retransmitted, or overlapping segments", err_string);
+}
+
+TEST(payload_injector_test, http_conflicting_s2c_traffic)
+{
+    Packet p(false);
+    set_configured();
+    p.flow = &flow;
+    p.active = &active;
+    p.packet_flags = PKT_FROM_CLIENT;
+    MockSession session(&flow, true);
+    flow.session = &session;
+    InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
+    CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_conflicting_s2c_traffic == 1);
+    CHECK(status == ERR_CONFLICTING_S2C_TRAFFIC);
+    CHECK(flow.flow_state == Flow::FlowState::BLOCK);
+    const char* err_string = PayloadInjector::get_err_string(status);
+    STRCMP_EQUAL("Conflicting S2C HTTP traffic in progress", err_string);
+    flow.session = nullptr;
+}
+
+TEST(payload_injector_test, http_s2c_proto_blocked)
+{
+    Packet p(false);
+    set_configured();
+    p.flow = &flow;
+    p.active = &active;
+    p.packet_flags = PKT_FROM_SERVER | PKT_HTTP_INJECT_BLOCKED;
+    InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
+    CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_s2c_http_proto == 1);
+    CHECK(status == ERR_S2C_HTTP_PROTO);
+    CHECK(flow.flow_state == Flow::FlowState::BLOCK);
+    const char* err_string = PayloadInjector::get_err_string(status);
+    STRCMP_EQUAL("HTTP/1 injection blocked on server response due to protocol state conflict",
+        err_string);
+}
+
+TEST(payload_injector_test, http_c2s_proto_blocked)
+{
+    Packet p(false);
+    set_configured();
+    p.flow = &flow;
+    p.active = &active;
+    p.packet_flags = PKT_HTTP_INJECT_BLOCKED;
+    InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
+    CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_c2s_http_proto == 1);
+    CHECK(status == ERR_C2S_HTTP_PROTO);
+    CHECK(flow.flow_state == Flow::FlowState::BLOCK);
+    const char* err_string = PayloadInjector::get_err_string(status);
+    STRCMP_EQUAL("HTTP/1 injection blocked on client request due to protocol state conflict",
+        err_string);
+}
+
+TEST(payload_injector_test, http2_conflicting_s2c_traffic)
+{
+    Packet p(false);
+    set_configured();
+    mock_api.base.name = "http2_inspect";
     flow.gadget = new MockInspector();
     p.flow = &flow;
+    p.active = &active;
+    p.packet_flags = PKT_FROM_CLIENT;
+    control.stream_id = 1;
+    MockSession session(&flow, true);
+    flow.session = &session;
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
-    CHECK(status == ERR_PKT_FROM_SERVER);
+    CHECK(counts->http_injects == 0);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_conflicting_s2c_traffic == 1);
+    CHECK(status == ERR_CONFLICTING_S2C_TRAFFIC);
     CHECK(flow.flow_state == Flow::FlowState::BLOCK);
+    const char* err_string = PayloadInjector::get_err_string(status);
+    STRCMP_EQUAL("Conflicting S2C HTTP traffic in progress", err_string);
     delete flow.gadget;
+    flow.session = nullptr;
 }
 
-TEST(payload_injector_test, flow_is_null)
+TEST(payload_injector_test, http2_s2c_proto_blocked)
 {
     Packet p(false);
     set_configured();
+    mock_api.base.name = "http2_inspect";
+    flow.gadget = new MockInspector();
+    p.flow = &flow;
+    p.active = &active;
+    p.packet_flags = PKT_FROM_SERVER | PKT_HTTP_INJECT_BLOCKED;
+    control.stream_id = 1;
     InjectionReturnStatus status = PayloadInjector::inject_http_payload(&p, control);
     CHECK(counts->http_injects == 0);
-    CHECK(status == ERR_UNIDENTIFIED_PROTOCOL);
+    CHECK(counts->failed_injects == 1);
+    CHECK(counts->err_s2c_http2_proto == 1);
+    CHECK(status == ERR_S2C_HTTP2_PROTO);
     const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string, "Unidentified protocol") == 0);
+    STRCMP_EQUAL("HTTP/2 injection blocked on server response due to protocol state conflict",
+        err_string);
+    delete flow.gadget;
 }
 
 TEST_GROUP(payload_injector_translate_err_test)
@@ -386,6 +552,7 @@ TEST_GROUP(payload_injector_translate_err_test)
         control.http_page_len = 4;
         flow.set_state(Flow::FlowState::INSPECT);
         flow.set_session_flags(SSNFLAG_ESTABLISHED);
+        flow.pkt_type = PktType::TCP;
         http2_flow_data.set_mid_frame(false);
         mock_api.base.name = "http2_inspect";
         flow.gadget = new MockInspector();
@@ -409,8 +576,8 @@ TEST(payload_injector_translate_err_test, http2_page_translation_err)
     translation_status = ERR_PAGE_TRANSLATION;
     status = PayloadInjector::inject_http_payload(&p, control);
     const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string, "Error in translating HTTP block page to HTTP/2. "
-        "Unsupported or bad format.") == 0);
+    STRCMP_EQUAL("Error in translating HTTP block page to HTTP/2. "
+        "Unsupported or bad format.", err_string);
 }
 
 TEST(payload_injector_translate_err_test, http2_hdrs_size)
@@ -421,23 +588,11 @@ TEST(payload_injector_translate_err_test, http2_hdrs_size)
     translation_status = ERR_TRANSLATED_HDRS_SIZE;
     status = PayloadInjector::inject_http_payload(&p, control);
     const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string,
-        "HTTP/2 translated header size is bigger than expected. Update max size.") == 0);
+    STRCMP_EQUAL("HTTP/2 translated header size is bigger than expected. Update max size.",
+        err_string);
 }
 
-TEST(payload_injector_translate_err_test, conflicting_s2c_traffic)
-{
-    Packet p(false);
-    set_configured();
-    p.flow = &flow;
-    translation_status = ERR_CONFLICTING_S2C_TRAFFIC;
-    status = PayloadInjector::inject_http_payload(&p, control);
-    const char* err_string = PayloadInjector::get_err_string(status);
-    CHECK(strcmp(err_string,
-        "Conflicting S2C HTTP traffic in progress") == 0);
-}
 int main(int argc, char** argv)
 {
     return CommandLineTestRunner::RunAllTests(argc, argv);
 }
-
index acc4d5f19e7e7cd9a8c18557cb320c1a68a62fa3..92d851033d70456b0482c874cf65ffde76c6220f 100644 (file)
@@ -90,6 +90,13 @@ class SFDAQInstance;
 
 #define PKT_TCP_PSEUDO_EST        0x80000000 // A one-sided or bidirectional without LWS TCP session was detected
 
+#define PKT_TCP_INJECT_BLOCKED    0x0000000100000000ULL  // cannot be injected on react due to tcp packet creates a hole,
+                                             // fills a hole, has overlaps or is retransmission
+
+// used by payload_injector
+#define PKT_HTTP_INJECT_ALLOWED  0x0000000200000000ULL
+#define PKT_HTTP_INJECT_BLOCKED  0x0000000400000000ULL
+
 #define TS_PKT_OFFLOADED          0x01
 #define TS_PKT_INJECT             0x02
 
@@ -125,7 +132,7 @@ struct SO_PUBLIC Packet
     Endianness* endianness = nullptr;
     Obfuscator* obfuscator = nullptr;
 
-    uint32_t packet_flags;      /* special flags for the packet */
+    uint64_t packet_flags;      /* special flags for the packet */
     uint32_t xtradata_mask;
     uint32_t proto_bits;        /* protocols contained within this packet */
 
@@ -340,6 +347,12 @@ struct SO_PUBLIC Packet
     bool was_set() const
     { return (packet_flags & PKT_WAS_SET) != 0; }
 
+    bool is_http_inject_permission_unset() const
+    {
+        return !(packet_flags & PKT_HTTP_INJECT_BLOCKED) and
+            !(packet_flags & PKT_HTTP_INJECT_ALLOWED);
+    }
+
     bool is_detection_enabled(bool to_server);
 
     bool is_inter_group_flow() const
index 134d1ee71305295042a624982c2954fbc50ac60c..8402cfbe144108431e603631cea8de211a237c0d 100644 (file)
@@ -97,7 +97,7 @@ bool HttpInspect::get_buf(snort::InspectionBuffer::Type, snort::Packet*, snort::
 const uint8_t* HttpInspect::adjust_log_packet(snort::Packet*, uint16_t&) { return nullptr; }
 StreamSplitter::Status HttpStreamSplitter::scan(snort::Packet*, const uint8_t*, uint32_t, uint32_t, uint32_t*)
 { return StreamSplitter::FLUSH; }
-StreamSplitter::Status HttpStreamSplitter::scan(snort::Flow*, const uint8_t*, uint32_t, uint32_t*)
+StreamSplitter::Status HttpStreamSplitter::scan(snort::Flow*, const uint8_t*, uint32_t, uint32_t*, snort::Packet*)
 { return StreamSplitter::FLUSH; }
 const snort::StreamBuffer HttpStreamSplitter::reassemble(snort::Flow*, unsigned, unsigned, const
     uint8_t*, unsigned, uint32_t, unsigned&)
index 6362509e0bc2b3fe7f3709b4118e5f2cefbd353b..d772d1b77b4c1cb658d1eea6c03eb3a093bc511f 100644 (file)
@@ -102,3 +102,40 @@ Dynamically allocated objects related to http_inspect are considered separate an
 included. Temporary objects (frame_data and frame_header) are ignored. The remaining dynamically
 allocated are Http2Infractions (8 bytes * 2) and Http2EventsGen(24 bytes * 2)
 Therefore, the memory required by http2 per flow: sizeof(Http2FlowData) + 1645 + 16 + 48 
+
+*** HTTP/2 Block Page Injection ***
+During server-to-client scan(), H2I determines whether block page injection is structurally
+possible for the current packet and sets PKT_HTTP_INJECT_BLOCKED or PKT_HTTP_INJECT_ALLOWED on the
+packet flags. The injection status is tracked per stream in a thread-local cache that is reset at
+the start of each scan call when no injection permission has been set yet (i.e., the first time
+scan() is called for this packet). A global_injection_block flag short-circuits further checks
+once the scan enters a state where injection is structurally impossible.
+
+PKT_HTTP_INJECT_BLOCKED is set in the following cases:
+
+Per-stream injection status (set via update_injection_status). Only the first frame for
+each stream ID in a packet is checked:
+1. Too many streams tracked - the per-thread injection status cache has reached
+   MAX_STREAM_INJECTION_STATUS entries. Further streams are blocked to cap resource usage.
+   This is a sanity check; usually one packet will have only 1-3 different HTTP/2 streams.
+2. Frame type is not HEADERS - injection is only allowed when the server-to-client frame for the
+   stream is a HEADERS frame. Data, settings, window update, and other frame types block injection
+   because the client won't be able to interpret the block page.
+3. Server stream state is not STREAM_EXPECT_HEADERS - even if the frame is HEADERS, the server
+   direction of the stream must be expecting headers (i.e., no prior server frames on this stream).
+   If the server has already sent headers or data (e.g., when processing trailers), injection is
+   blocked.
+4. Stream not found and no matching client stream - if the stream object does not exist and there
+   is no unflushed client data for this stream ID, injection is blocked.
+
+Structural injection block (global_injection_block is set to true, skipping all further
+per-stream checks for the remainder of the scan call):
+
+5. Packet starts with trailing frame padding - the scan is discarding padding that trails a
+   previous frame.
+6. Packet starts with padding length byte - the scan is reading the padding length field of a
+   padded frame.
+7. Packet starts with data frame body - the scan is processing frame data
+
+Cases 5-7 all indicate that the server-to-client flow is in the middle of a frame, making
+injection structurally impossible.
index 43213b2fba7dc1459778dfd72cd8c2e63ae3ee48..f71a2ad022830464f4b12fa5a187c79472921529 100644 (file)
@@ -32,6 +32,10 @@ static const uint32_t NO_STREAM_ID = 0xFFFFFFFF;
 // Perform memory allocation and deallocation tracking for Http2Stream objects in increments of 25
 static const uint32_t STREAM_MEMORY_TRACKING_INCREMENT = 25;
 
+// Maximum number of distinct stream IDs tracked per scan for injection status.
+// If exceeded, injection is blocked to avoid expensive stream lookups.
+static const uint32_t MAX_STREAM_INJECTION_STATUS = 20;
+
 static const uint32_t HTTP2_GID = 121;
 
 // Frame type codes (fourth octet of frame header)
@@ -193,6 +197,8 @@ enum SettingsFrameIds
 };
 
 enum ScanState { SCAN_FRAME_HEADER, SCAN_PADDING_LENGTH, SCAN_DATA, SCAN_EMPTY_DATA };
+
+enum InjectionStatus { INJECTION_BLOCKED = 0, INJECTION_ALLOWED = 1 };
 } // end namespace Http2Enums
 
 #endif
index 28dca58870a5dc3c3371d991391108f550f58079..1da56552b642ab0136430ca0ac0448ed3f2d8b08 100644 (file)
@@ -21,7 +21,6 @@
 #define HTTP2_FLOW_DATA_H
 
 #include <queue>
-#include <vector>
 
 #include "flow/flow.h"
 #include "flow/stream_flow.h"
index 768a7bb42999cafb9e6212375b4e339fd274be60..7160639ab38ad61e4e5e8a4dea45d7d9fa7288f9 100644 (file)
@@ -101,7 +101,7 @@ StreamSplitter::Status Http2StreamSplitter::scan(Packet* pkt, const uint8_t* dat
         return HttpStreamSplitter::status_value(StreamSplitter::ABORT, true);
 
     StreamSplitter::Status ret_val =
-        implement_scan(session_data, data, length, flush_offset, source_id);
+        implement_scan(session_data, data, length, flush_offset, source_id, pkt);
 
     session_data->bytes_scanned[source_id] += (ret_val == StreamSplitter::FLUSH)?
         *flush_offset : length;
index e9013868cdff6c3d05d4927735707af2c8c752b4..2f1ad449f6daf9f9100568aa1c2d09d029f09c64 100644 (file)
@@ -54,12 +54,16 @@ private:
         uint32_t length, uint32_t* flush_offset, HttpCommon::SourceId source_id, uint8_t type,
         uint8_t frame_flags, uint32_t& data_offset);
     static snort::StreamSplitter::Status implement_scan(Http2FlowData* session_data, const uint8_t* data,
-        uint32_t length, uint32_t* flush_offset, HttpCommon::SourceId source_id);
+        uint32_t length, uint32_t* flush_offset, HttpCommon::SourceId source_id, snort::Packet* pkt);
     static const snort::StreamBuffer implement_reassemble(Http2FlowData* session_data, unsigned total,
         unsigned offset, const uint8_t* data, unsigned len, uint32_t flags,
         HttpCommon::SourceId source_id);
     static bool read_frame_hdr(Http2FlowData* session_data, const uint8_t* data,
         uint32_t length, HttpCommon::SourceId source_id, uint32_t& data_offset);
+    static Http2Enums::InjectionStatus determine_injection_status(Http2FlowData* session_data,
+        uint8_t frame_type, uint32_t stream_id);
+    static void update_injection_status(Http2FlowData* session_data, snort::Packet* pkt,
+        uint8_t frame_type);
 };
 
 
index 5c9180176a91452705ba05905d0cc8de33d0941b..58b8ddc7c4c30ac79c2e5389c0cb10947dc5f090 100644 (file)
@@ -23,6 +23,8 @@
 
 #include "http2_stream_splitter.h"
 
+#include <algorithm>
+#include <array>
 #include <cassert>
 
 #include "service_inspectors/http_inspect/http_common.h"
@@ -39,6 +41,11 @@ using namespace snort;
 using namespace HttpCommon;
 using namespace Http2Enums;
 
+static THREAD_LOCAL std::array<std::pair<uint32_t, Http2Enums::InjectionStatus>,
+    Http2Enums::MAX_STREAM_INJECTION_STATUS> stream_injection_status;
+static THREAD_LOCAL uint32_t stream_injection_status_count = 0;
+static THREAD_LOCAL bool global_injection_block = false;
+
 enum ValidationResult { V_GOOD, V_BAD, V_TBD };
 
 static ValidationResult validate_preface(const uint8_t* data, const uint32_t length,
@@ -63,6 +70,59 @@ static ValidationResult validate_preface(const uint8_t* data, const uint32_t len
     return V_GOOD;
 }
 
+Http2Enums::InjectionStatus Http2StreamSplitter::determine_injection_status(Http2FlowData* session_data,
+    uint8_t frame_type, uint32_t stream_id)
+{
+    if (frame_type != FT_HEADERS)
+        return Http2Enums::INJECTION_BLOCKED;
+
+    const Http2Stream* const stream = session_data->find_stream(stream_id);
+
+    if (!stream)
+    {
+        // Check if we have some unflushed data from client
+        const uint32_t client_stream_id = session_data->current_stream[SRC_CLIENT];
+        return (client_stream_id == stream_id) ?
+            Http2Enums::INJECTION_ALLOWED : Http2Enums::INJECTION_BLOCKED;
+    }
+
+    return (stream->get_state(SRC_SERVER) == STREAM_EXPECT_HEADERS) ?
+        Http2Enums::INJECTION_ALLOWED : Http2Enums::INJECTION_BLOCKED;
+}
+
+void Http2StreamSplitter::update_injection_status(Http2FlowData* session_data, Packet* pkt,
+    uint8_t frame_type)
+{
+    pkt->packet_flags &= ~(PKT_HTTP_INJECT_ALLOWED | PKT_HTTP_INJECT_BLOCKED);
+
+    if (stream_injection_status_count >= MAX_STREAM_INJECTION_STATUS)
+    {
+        pkt->packet_flags |= PKT_HTTP_INJECT_BLOCKED;
+        return;
+    }
+
+    const uint32_t current_stream_id = session_data->current_stream[SRC_SERVER];
+    const auto begin = stream_injection_status.begin();
+    const auto end = begin + stream_injection_status_count;
+    auto it = std::find_if(begin, end,
+        [current_stream_id](const std::pair<uint32_t, Http2Enums::InjectionStatus>& entry)
+        { return entry.first == current_stream_id; });
+
+    Http2Enums::InjectionStatus verdict;
+    if (it != end)
+    {
+        verdict = it->second;
+    }
+    else
+    {
+        verdict = determine_injection_status(session_data, frame_type, current_stream_id);
+        stream_injection_status[stream_injection_status_count++] = { current_stream_id, verdict };
+    }
+
+    pkt->packet_flags |= (verdict == Http2Enums::INJECTION_ALLOWED) ?
+        PKT_HTTP_INJECT_ALLOWED : PKT_HTTP_INJECT_BLOCKED;
+}
+
 void Http2StreamSplitter::data_frame_header_checks(Http2FlowData* session_data,
      HttpCommon::SourceId source_id)
 {
@@ -135,7 +195,7 @@ bool Http2StreamSplitter::read_frame_hdr(Http2FlowData* session_data, const uint
 }
 
 StreamSplitter::Status Http2StreamSplitter::implement_scan(Http2FlowData* session_data,
-    const uint8_t* data, uint32_t length, uint32_t* flush_offset, HttpCommon::SourceId source_id)
+    const uint8_t* data, uint32_t length, uint32_t* flush_offset, HttpCommon::SourceId source_id, Packet* pkt)
 {
     if (session_data->preface[source_id])
     {
@@ -166,6 +226,12 @@ StreamSplitter::Status Http2StreamSplitter::implement_scan(Http2FlowData* sessio
     *flush_offset = 0;
     uint32_t data_offset = 0;
 
+    if (source_id == SRC_SERVER and pkt->is_http_inject_permission_unset())
+    {
+        stream_injection_status_count = 0;
+        global_injection_block = false;
+    }
+
     // Need to process multiple frames in a single scan() if a single TCP segment has multiple
     // header and continuation frames
     while ((status == StreamSplitter::SEARCH) &&
@@ -186,6 +252,11 @@ StreamSplitter::Status Http2StreamSplitter::implement_scan(Http2FlowData* sessio
                     session_data->scan_remaining_frame_octets[source_id] -= avail;
                     session_data->payload_discard[source_id] = true;
                     *flush_offset = avail;
+                    if (source_id == SRC_SERVER and pkt->is_http_inject_permission_unset())
+                    {
+                        pkt->packet_flags |= PKT_HTTP_INJECT_BLOCKED;
+                        global_injection_block = true;
+                    }
                     return StreamSplitter::FLUSH;
                 }
 
@@ -198,6 +269,9 @@ StreamSplitter::Status Http2StreamSplitter::implement_scan(Http2FlowData* sessio
                 session_data->current_stream[source_id] =
                     get_stream_id_from_header(session_data->scan_frame_header[source_id]);
 
+                if (source_id == SRC_SERVER and !global_injection_block)
+                    update_injection_status(session_data, pkt, type);
+
                 if (session_data->continuation_expected[source_id] &&
                     ((type != FT_CONTINUATION) ||
                      (old_stream_id != session_data->current_stream[source_id])))
@@ -284,6 +358,12 @@ StreamSplitter::Status Http2StreamSplitter::implement_scan(Http2FlowData* sessio
                 break;
             }
             case SCAN_PADDING_LENGTH:
+                if (source_id == SRC_SERVER and pkt->is_http_inject_permission_unset())
+                {
+                    pkt->packet_flags |= PKT_HTTP_INJECT_BLOCKED;
+                    global_injection_block = true;
+                }
+
                 assert(session_data->scan_remaining_frame_octets[source_id] > 0);
                 session_data->padding_length[source_id] = *(data + data_offset);
                 if (session_data->frame_type[source_id] == FT_DATA)
@@ -314,6 +394,12 @@ StreamSplitter::Status Http2StreamSplitter::implement_scan(Http2FlowData* sessio
             case SCAN_DATA:
             case SCAN_EMPTY_DATA:
             {
+                if (source_id == SRC_SERVER and pkt->is_http_inject_permission_unset())
+                {
+                    pkt->packet_flags |= PKT_HTTP_INJECT_BLOCKED;
+                    global_injection_block = true;
+                }
+
                 const uint8_t type = get_frame_type(session_data->scan_frame_header[source_id]);
                 const uint8_t frame_flags = get_frame_flags(session_data->
                     scan_frame_header[source_id]);
index d6f0c0035fc386af9cedcf28c5947e10b351825b..eaa1ffda184725ddd49f9684fcf9bd4a6817b004 100755 (executable)
@@ -65,3 +65,7 @@ include::dev_notes_mime_inspection.txt[]
 ==== HI test tool usage
 
 include::dev_notes_test_tool.txt[]
+
+==== HTTP/1 Block Page Injection
+
+include::dev_notes_block_page_injection.txt[]
diff --git a/src/service_inspectors/http_inspect/dev_notes_block_page_injection.txt b/src/service_inspectors/http_inspect/dev_notes_block_page_injection.txt
new file mode 100644 (file)
index 0000000..b23a4e5
--- /dev/null
@@ -0,0 +1,7 @@
+During scan(), HI determines whether block page injection is possible for the current packet and
+sets PKT_HTTP_INJECT_ALLOWED or PKT_HTTP_INJECT_BLOCKED on the packet flags. This check is
+performed once per packet when no injection permission has been set yet.
+
+PKT_HTTP_INJECT_ALLOWED is set when the server side is expecting a fresh response, i.e.,
+type_expected is SEC_STATUS and no response cutter has been created yet. In all other cases
+PKT_HTTP_INJECT_BLOCKED is set.
\ No newline at end of file
index 14208ba46e0af36580c6b4215046dd42f75f8dd4..0079cbf5272a943524995b8b2ef436ab840d6a9f 100644 (file)
@@ -39,7 +39,8 @@ public:
         source_id(is_client_to_server ? HttpCommon::SRC_CLIENT : HttpCommon::SRC_SERVER) {}
     Status scan(snort::Packet* pkt, const uint8_t* data, uint32_t length, uint32_t not_used,
         uint32_t* flush_offset) override;
-    Status scan(snort::Flow* flow, const uint8_t* data, uint32_t length, uint32_t* flush_offset) override;
+    Status scan(snort::Flow* flow, const uint8_t* data, uint32_t length, uint32_t* flush_offset,
+        snort::Packet* pkt = nullptr) override;
     const snort::StreamBuffer reassemble(snort::Flow* flow, unsigned total, unsigned, const
         uint8_t* data, unsigned len, uint32_t flags, unsigned& copied) override;
     bool finish(snort::Flow* flow) override;
index 3ba6c0c8aa11a46d183495f6abad92934f119f4a..b85434c35e27657958383bae97331fd5b29bcb8c 100644 (file)
@@ -30,7 +30,8 @@ public:
 
     virtual void prep_partial_flush(snort::Flow* flow, uint32_t num_flush) = 0;
 
-    virtual Status scan(snort::Flow* flow, const uint8_t* data, uint32_t length, uint32_t* flush_offset) = 0;
+    virtual Status scan(snort::Flow* flow, const uint8_t* data, uint32_t length, uint32_t* flush_offset,
+        snort::Packet* pkt = nullptr) = 0;
 protected:
     HttpStreamSplitterBase(bool c2s) : StreamSplitter(c2s) { }
 private:
index 0d66da6c17b64e3932d6880a17a9c384dd0aaae5..3bf24612a1b45f1e2bbfcc95a8bfdeef1b6d02a8 100644 (file)
@@ -133,7 +133,7 @@ StreamSplitter::Status HttpStreamSplitter::status_value(StreamSplitter::Status r
 StreamSplitter::Status HttpStreamSplitter::scan(Packet* pkt, const uint8_t* data, uint32_t length,
     uint32_t, uint32_t* flush_offset)
 {
-    return scan(pkt->flow, data, length, flush_offset);
+    return scan(pkt->flow, data, length, flush_offset, pkt);
 }
 
 StreamSplitter::Status HttpStreamSplitter::handle_zero_nine(Flow* flow, HttpFlowData* session_data, const uint8_t* data, uint32_t length,
@@ -302,7 +302,7 @@ StreamSplitter::Status HttpStreamSplitter::call_cutter(Flow* flow, HttpFlowData*
 }
 
 StreamSplitter::Status HttpStreamSplitter::scan(Flow* flow, const uint8_t* data, uint32_t length,
-    uint32_t* flush_offset)
+    uint32_t* flush_offset, Packet* pkt)
 {
     Profile profile(HttpModule::get_profile_stats()); // cppcheck-suppress unreadVariable
 
@@ -394,6 +394,15 @@ StreamSplitter::Status HttpStreamSplitter::scan(Flow* flow, const uint8_t* data,
 
     HttpModule::increment_peg_counts(PEG_SCAN);
 
+    if (pkt and pkt->is_http_inject_permission_unset())
+    {
+        if (session_data->type_expected[SRC_SERVER] == SEC_STATUS
+            and session_data->cutter[SRC_SERVER] == nullptr)
+            pkt->packet_flags |= PKT_HTTP_INJECT_ALLOWED;
+        else
+            pkt->packet_flags |= PKT_HTTP_INJECT_BLOCKED;
+    }
+
     return call_cutter(flow, session_data, data, length, flush_offset, type);
 }
 
index bbff69882907d4d34b52f650f1930eb136794c4b..959706d78b024edb0574dd72471699c1a81aface 100644 (file)
@@ -106,7 +106,7 @@ bool HttpInspect::get_buf(snort::InspectionBuffer::Type, snort::Packet*, snort::
 const uint8_t* HttpInspect::adjust_log_packet(snort::Packet*, uint16_t&) { return nullptr; }
 StreamSplitter::Status HttpStreamSplitter::scan(snort::Packet*, const uint8_t*, uint32_t, uint32_t, uint32_t*)
 { return StreamSplitter::FLUSH; }
-StreamSplitter::Status HttpStreamSplitter::scan(snort::Flow*, const uint8_t*, uint32_t, uint32_t*)
+StreamSplitter::Status HttpStreamSplitter::scan(snort::Flow*, const uint8_t*, uint32_t, uint32_t*, Packet*)
 { return StreamSplitter::FLUSH; }
 const snort::StreamBuffer HttpStreamSplitter::reassemble(snort::Flow*, unsigned, unsigned, const
     uint8_t*, unsigned, uint32_t, unsigned&)
index 80cd35a21feaa812f5048a418e55773f8f117296..969d06d3906a7411ce30768a32c5be7c867ca3c3 100644 (file)
@@ -194,6 +194,9 @@ void TcpReassemblySegments::insert_segment_in_seglist(TcpSegmentDescriptor& tsd)
         return;
     }
 
+    // Packet is problematic, may have overlaps/retransmission, fill the hole, create a hole
+    tsd.set_packet_flags(PKT_TCP_INJECT_BLOCKED);
+
     tos->init(tsd);
     overlap_resolver->eval_left(*tos);
     overlap_resolver->eval_right(*tos);
index ee019a28b7f2c4ee45e82a3e3d36c379b262f444..4b0d5bea128273c6628055f9083def550afda268 100644 (file)
@@ -128,10 +128,10 @@ public:
         pkt->dsize -= offset;
     }
 
-    void set_packet_flags(uint32_t flags) const
+    void set_packet_flags(uint64_t flags) const
     { pkt->packet_flags |= flags; }
 
-    bool are_packet_flags_set(uint32_t flags) const
+    bool are_packet_flags_set(uint64_t flags) const
     { return (pkt->packet_flags & flags) == flags; }
 
     uint32_t get_packet_timestamp() const