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
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;
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) )
{
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
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,
TextLog_Term(tlog);
}
}
-
// 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
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.
{ 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,
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
// 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;
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);
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
};
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 }
};
{
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;
#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"
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;
{
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);
}
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)
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);
}
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);
}
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;
}
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;
}
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;
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)
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();
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)
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);
}
-
#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
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 */
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
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&)
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.
// 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)
};
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
#define HTTP2_FLOW_DATA_H
#include <queue>
-#include <vector>
#include "flow/flow.h"
#include "flow/stream_flow.h"
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;
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);
};
#include "http2_stream_splitter.h"
+#include <algorithm>
+#include <array>
#include <cassert>
#include "service_inspectors/http_inspect/http_common.h"
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,
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)
{
}
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])
{
*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) &&
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;
}
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])))
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)
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]);
==== HI test tool usage
include::dev_notes_test_tool.txt[]
+
+==== HTTP/1 Block Page Injection
+
+include::dev_notes_block_page_injection.txt[]
--- /dev/null
+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
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;
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:
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,
}
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
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);
}
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&)
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);
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