]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #3218: US #684704: http_inspect: improve version processing
authorTom Peters (thopeter) <thopeter@cisco.com>
Mon, 10 Jan 2022 19:46:58 +0000 (19:46 +0000)
committerTom Peters (thopeter) <thopeter@cisco.com>
Mon, 10 Jan 2022 19:46:58 +0000 (19:46 +0000)
Merge in SNORT/snort3 from ~MDAGON/snort3:version to master

Squashed commit of the following:

commit 678d5e1729f67abcbe05886aefc60485ff7e9d27
Author: Maya Dagon <mdagon@cisco.com>
Date:   Tue Nov 30 15:57:27 2021 -0500

    http_inspect: version update, http_version_match rule option

12 files changed:
doc/reference/builtin_stubs.txt
doc/user/http_inspect.txt
src/pub_sub/test/pub_sub_http_request_body_event_test.cc
src/service_inspectors/http_inspect/http_api.cc
src/service_inspectors/http_inspect/http_enum.h
src/service_inspectors/http_inspect/http_flow_data.h
src/service_inspectors/http_inspect/http_msg_start.cc
src/service_inspectors/http_inspect/http_stream_splitter_scan.cc
src/service_inspectors/http_inspect/http_tables.cc
src/service_inspectors/http_inspect/http_transaction.cc
src/service_inspectors/http_inspect/ips_http.cc
src/service_inspectors/http_inspect/ips_http.h

index bbad9a9238504fce01b9e9151a07c00674b6ec69..0254995dd8f229ca37d43a521109336fa718b8ec 100644 (file)
@@ -939,11 +939,6 @@ request or status line.
 The HTTP version in the start line begins with "HTTP/" but the remainder is not in the expected
 <digit>.<digit> format.
 
-119:208
-
-The HTTP version in the start line has a valid format but is not HTTP/1.0 or HTTP/1.1. This alert
-does not apply to HTTP/2 or HTTP/3 traffic.
-
 119:209
 
 An HTTP header line contains a format error. A well-formed header consists of a field name followed
@@ -1287,6 +1282,19 @@ this alert is raised. This alert is not expected for typical network traffic and
 an indication that an attacker is trying to exhaust resources. This alert is raised
 by the enhanced JavaScript normalizer.
 
+119:275
+
+The HTTP version in the start line has a valid 1.<subversion> format, but the subversion is not 0 or 1.
+
+119:276
+
+The HTTP version in the start line has a valid format but the version is 0.
+
+119:277
+
+The HTTP version in the start line has a valid format but the version is higher than 1. This alert
+does not apply to HTTP/2 or HTTP/3 traffic.
+
 121:1
 
 Invalid flag set on HTTP/2 frame header
index c32787ccfe1e7bb2e5d3417d02a2c3f2b3f7f5a4..d47ed264101aa413bea80033c28d0ab48cbf82b1 100755 (executable)
@@ -692,6 +692,20 @@ trailers, respectively. Checks available: equal to "=" or just value,
 not "!" or "!=",  less than "<", greater than ">", less or equal to "<=",
 less or greater than ">=", in range "<>", in range or equal to "<=>".
 
+===== http_version_match
+
+Rule option that matches HTTP version to one of the listed version values.
+Possible match values: 1.0, 1.1, 2.0, 0.9, other, and malformed.
+When receiving a request line or status line, if the version is present
+it will be used for comparison. If the version doesn't have a format of
+[0-9].[0-9] it is considered malformed. A [0-9].[0-9] that is not 1.0 or 1.1
+is considered other. 0.9 refers to the original HTTP protocol version that
+uses simple GET requests without headers and includes no version number. 2.0
+refers to the actual HTTP/2 protocol with framed data. Messages that follow
+the general HTTP/1 format but contain version fields falsely claiming to be
+HTTP/2.0 or HTTP/0.9 will match "other" as described above. The http_version
+rule option is available to examine the actual bytes in the version field.
+
 ==== Timing issues and combining rule options
 
 HTTP inspector is stateful. That means it is aware of a bigger picture than
index 173a3d638d04889406505ce122c994846bde6d81..ea84a7d957e4dddd23028e7c76b463ba23f701f1 100644 (file)
@@ -73,7 +73,7 @@ HttpMsgSection::HttpMsgSection(const uint8_t* buffer, const uint16_t buf_size,
     trans_num(STAT_NOT_PRESENT),
     status_code_num(STAT_NOT_PRESENT),
     source_id(source_id_),
-    version_id(VERS__NO_SOURCE),
+    version_id(VERS__NOT_PRESENT),
     method_id(METH__NOT_PRESENT),
     tcp_close(false)
 {}
index 91c1327b96fba161fced1d861195efe8ba82b2e9..a1caead7b98c546e942b8be993dfa82589e40baa 100644 (file)
@@ -120,6 +120,7 @@ extern const BaseApi* ips_http_trailer;
 extern const BaseApi* ips_http_true_ip;
 extern const BaseApi* ips_http_uri;
 extern const BaseApi* ips_http_version;
+extern const BaseApi* ips_http_version_match;
 extern const BaseApi* ips_js_data;
 
 #ifdef BUILDING_SO
@@ -149,6 +150,7 @@ const BaseApi* sin_http[] =
     ips_http_true_ip,
     ips_http_uri,
     ips_http_version,
+    ips_http_version_match,
     ips_js_data,
     nullptr
 };
index 11ac86059a3ad128e7a10174e047533cba5d0eb7..372aaf145106b589556bbdb90ab5caac8dc184ad 100755 (executable)
@@ -61,8 +61,8 @@ enum HTTP_RULE_OPT { HTTP_BUFFER_CLIENT_BODY = 1, HTTP_BUFFER_COOKIE, HTTP_BUFFE
     HTTP_BUFFER_RAW_HEADER, HTTP_BUFFER_RAW_REQUEST, HTTP_BUFFER_RAW_STATUS,
     HTTP_BUFFER_RAW_TRAILER, HTTP_BUFFER_RAW_URI, HTTP_BUFFER_STAT_CODE, HTTP_BUFFER_STAT_MSG,
     HTTP_BUFFER_TRAILER, HTTP_BUFFER_TRUE_IP, HTTP_BUFFER_URI, HTTP_BUFFER_VERSION,
-    BUFFER_JS_DATA, BUFFER_VBA_DATA , HTTP_BUFFER_MAX = BUFFER_VBA_DATA,
-    HTTP_RANGE_NUM_HDRS, HTTP_RANGE_NUM_TRAILERS, HTTP_MAX_RULE_OPTION };
+    BUFFER_JS_DATA, BUFFER_VBA_DATA , HTTP__BUFFER_MAX = BUFFER_VBA_DATA,
+    HTTP_RANGE_NUM_HDRS, HTTP_RANGE_NUM_TRAILERS, HTTP_VERSION_MATCH, HTTP__MAX_RULE_OPTION };
 
 // Peg counts
 // This enum must remain synchronized with HttpModule::peg_names[] in http_tables.cc
@@ -83,8 +83,9 @@ enum ChunkState { CHUNK_NEWLINES, CHUNK_ZEROS, CHUNK_LEADING_WS, CHUNK_NUMBER, C
     CHUNK_OPTIONS, CHUNK_HCRLF, CHUNK_DATA, CHUNK_DCRLF1, CHUNK_DCRLF2, CHUNK_BAD };
 
 // List of possible HTTP versions.
-enum VersionId { VERS__NO_SOURCE=-16, VERS__NOT_COMPUTE=-14, VERS__PROBLEMATIC=-12,
-    VERS__NOT_PRESENT=-11, VERS__OTHER=1, VERS_1_0, VERS_1_1, VERS_2_0, VERS_0_9 };
+enum VersionId { VERS__PROBLEMATIC=-12, VERS__NOT_PRESENT=-11, VERS__OTHER=1,
+    VERS_1_0, VERS_1_1, VERS_2_0, VERS_0_9, VERS__MIN = VERS__PROBLEMATIC,
+    VERS__MAX = VERS_0_9};
 
 // Every request method we have ever heard of
 enum MethodId {
@@ -160,7 +161,7 @@ enum Infraction
     INF_TOO_MANY_HEADERS = 4,
     INF_BAD_HEADER = 5,
     INF_BAD_STAT_CODE = 6,
-    INF_UNKNOWN_VERSION = 7,
+    INF_VERSION_HIGHER_THAN_1 = 7,
     INF_BAD_VERSION = 8,
     INF_ZERO_NINE_NOT_FIRST = 9,
     INF_CODE_POINT_IN_URI = 10,
@@ -286,6 +287,8 @@ enum Infraction
     INF_ACCEPT_ENCODING_CONSECUTIVE_COMMAS = 130,
     INF_JS_PDU_MISS = 131,
     INF_JS_SCOPE_NEST_OVERFLOW = 132,
+    INF_INVALID_SUBVERSION = 133,
+    INF_VERSION_0 = 134,
     INF__MAX_VALUE
 };
 
@@ -352,7 +355,8 @@ enum EventSid
     EVENT_CTRL_IN_REASON = 205,
     EVENT_IMPROPER_WS = 206,
     EVENT_BAD_VERS = 207,
-    EVENT_UNKNOWN_VERS = 208,
+    // EVENT_VERSION_HIGHER_THAN_1 = 208,        // Retired. Do not reuse this number
+
     EVENT_BAD_HEADER = 209,
     EVENT_CHUNK_OPTIONS = 210,
     EVENT_URI_BAD_FORMAT = 211,
@@ -419,6 +423,9 @@ enum EventSid
     EVENT_ACCEPT_ENCODING_CONSECUTIVE_COMMAS = 272,
     EVENT_JS_PDU_MISS = 273,
     EVENT_JS_SCOPE_NEST_OVERFLOW = 274,
+    EVENT_INVALID_SUBVERSION = 275,
+    EVENT_VERSION_0 = 276,
+    EVENT_VERSION_HIGHER_THAN_1 = 277,
     EVENT__MAX_VALUE
 };
 
index c7ce3b6a366a290f7102150d9d2f64b729379675..29e6897c4a479acf536448da9d44bb388a8cf03f 100644 (file)
@@ -86,6 +86,9 @@ public:
 
     uint32_t get_h2_stream_id() const;
 
+    HttpEnums::VersionId get_version_id(HttpCommon::SourceId source_id) const
+    { return version_id[source_id]; }
+
 private:
     // HTTP/2 handling
     bool for_http2 = false;
index 685221863350e66eaec6af338b91a7fb19e35b5f..904366ef8d3e7fb6c1486b99f1307e4cbde28315 100644 (file)
@@ -52,40 +52,39 @@ void HttpMsgStart::derive_version_id()
     }
     else if ((version.start()[5] == '1') && (version.start()[7] == '1'))
     {
-        version_id = VERS_1_1;
+        if (session_data->for_http2)
+            version_id = VERS_2_0;
+        else
+            version_id = VERS_1_1;
     }
     else if ((version.start()[5] == '1') && (version.start()[7] == '0'))
     {
         version_id = VERS_1_0;
     }
-    else if ((version.start()[5] == '2') && (version.start()[7] == '0'))
+    else if ((version.start()[5] < '0') || (version.start()[5] > '9') ||
+        (version.start()[7] < '0') || (version.start()[7] > '9'))
     {
-        version_id = VERS_2_0;
+        version_id = VERS__PROBLEMATIC;
+        add_infraction(INF_BAD_VERSION);
+        create_event(EVENT_BAD_VERS);
     }
-    else if ((version.start()[5] == '0') && (version.start()[7] == '9'))
+    else if ((version.start()[5] > '1') && (version.start()[5] <= '9'))
     {
-        // Real 0.9 traffic would never be labeled HTTP/0.9 because 0.9 is older than the version
-        // system. Aside from the possibility that someone might do this to make trouble,
-        // HttpStreamSplitter::reassemble() converts 0.9 responses to a simple form of 1.0 format
-        // to allow us to process 0.9 without a lot of extra development. Such responses are
-        // labeled 0.9.
-        // FIXIT-M the 0.9 trick opens the door to someone spoofing us with a real start line
-        // labeled HTTP/0.9. Need to close this weakness.
-        // FIXIT-M similarly "HTTP/2.0" is not a legitimate thing we could actually see.
-        version_id = VERS_0_9;
+        version_id = VERS__OTHER;
+        add_infraction(INF_VERSION_HIGHER_THAN_1);
+        create_event(EVENT_VERSION_HIGHER_THAN_1);
     }
-    else if ((version.start()[5] >= '0') && (version.start()[5] <= '9') &&
-        (version.start()[7] >= '0') && (version.start()[7] <= '9'))
+    else if (version.start()[5] == '1')
     {
         version_id = VERS__OTHER;
-        add_infraction(INF_UNKNOWN_VERSION);
-        create_event(EVENT_UNKNOWN_VERS);
+        add_infraction(INF_INVALID_SUBVERSION);
+        create_event(EVENT_INVALID_SUBVERSION);
     }
     else
     {
-        version_id = VERS__PROBLEMATIC;
-        add_infraction(INF_BAD_VERSION);
-        create_event(EVENT_BAD_VERS);
+        version_id = VERS__OTHER;
+        add_infraction(INF_VERSION_0);
+        create_event(EVENT_VERSION_0);
     }
 }
 
index eff62701dbf01ce6ad50916118b55f24c393956d..98a79d0bf2750dd359c865bd9a3e8025be7020df 100644 (file)
@@ -237,12 +237,12 @@ StreamSplitter::Status HttpStreamSplitter::scan(Packet* pkt, const uint8_t* data
     if ((type == SEC_STATUS) &&
         (session_data->expected_trans_num[SRC_SERVER] == session_data->zero_nine_expected))
     {
-        // 0.9 response is a body that runs to connection end with no headers. HttpInspect does
-        // not support no headers. Processing this imaginary status line and empty headers allows
+        // 0.9 response is a body that runs to connection end with no headers.
+        // Processing this imaginary empty headers allows
         // us to overcome this limitation and reuse the entire HTTP infrastructure.
-        prepare_flush(session_data, nullptr, SEC_STATUS, 14, 0, 0, false, 0, 14);
-        my_inspector->process((const uint8_t*)"HTTP/0.9 200 .", 14, flow, SRC_SERVER, false);
-        session_data->transaction[SRC_SERVER]->clear_section();
+        session_data->version_id[source_id] = VERS_0_9;
+        session_data->status_code_num = 200;
+        HttpModule::increment_peg_counts(PEG_RESPONSE);
         prepare_flush(session_data, nullptr, SEC_HEADER, 0, 0, 0, false, 0, 0);
         my_inspector->process((const uint8_t*)"", 0, flow, SRC_SERVER, false);
         session_data->transaction[SRC_SERVER]->clear_section();
index 1ff3885885e3376562d514f64941163cb5498e2c..4f01f8506f30d734dedab11225693eadef666b05 100755 (executable)
@@ -261,7 +261,6 @@ const RuleMap HttpModule::http_events[] =
     { EVENT_CTRL_IN_REASON,             "control character in HTTP response reason phrase" },
     { EVENT_IMPROPER_WS,                "illegal extra whitespace in start line" },
     { EVENT_BAD_VERS,                   "corrupted HTTP version" },
-    { EVENT_UNKNOWN_VERS,               "HTTP version in start line is not HTTP/1.0 or 1.1" },
     { EVENT_BAD_HEADER,                 "format error in HTTP header" },
     { EVENT_CHUNK_OPTIONS,              "chunk header options present" },
     { EVENT_URI_BAD_FORMAT,             "URI badly formatted" },
@@ -335,6 +334,9 @@ const RuleMap HttpModule::http_events[] =
                                         "header" },
     { EVENT_JS_PDU_MISS,                "missed PDUs during JavaScript normalization" },
     { EVENT_JS_SCOPE_NEST_OVERFLOW,     "JavaScript scope nesting is over capacity" },
+    { EVENT_INVALID_SUBVERSION,         "HTTP/1 version other than 1.0 or 1.1" },
+    { EVENT_VERSION_0,                  "HTTP version in start line is 0" },
+    { EVENT_VERSION_HIGHER_THAN_1,      "HTTP version in start line is higher than 1" },
     { 0, nullptr }
 };
 
index 84ff5577634557a6e97004afdc2d6a19cf2b145c..c17a5f08a3b0644dffbf98de3532231775b2768c 100644 (file)
@@ -141,7 +141,9 @@ HttpTransaction* HttpTransaction::attach_my_transaction(HttpFlowData* session_da
     // pipeline is empty check for a request transaction and take it. If there is no transaction
     // available then declare an underflow and create a new transaction specifically for the
     // response side.
-    else if (session_data->section_type[source_id] == SEC_STATUS)
+    else if (session_data->section_type[source_id] == SEC_STATUS ||
+            (session_data->section_type[source_id] == SEC_HEADER &&
+            session_data->version_id[source_id] == VERS_0_9))
     {
         delete_transaction(session_data->transaction[SRC_SERVER], session_data);
         if (session_data->pipeline_underflow)
index 5603318bf4b10d1374f28f44e4200e2885c6b8bc..ffc64fa55c06cf8b318eb84b16b7e9f411ef5756 100644 (file)
@@ -69,6 +69,7 @@ bool HttpRuleOptModule::begin(const char*, int, SnortConfig*)
     case HTTP_BUFFER_URI:
     case HTTP_BUFFER_VERSION:
     case HTTP_RANGE_NUM_HDRS:
+    case HTTP_VERSION_MATCH:
         inspect_section = IS_FLEX_HEADER;
         break;
     case HTTP_BUFFER_CLIENT_BODY:
@@ -87,6 +88,44 @@ bool HttpRuleOptModule::begin(const char*, int, SnortConfig*)
     return true;
 }
 
+static const std::map <std::string, VersionId> VersionStrToEnum =
+{
+    { "malformed", VERS__PROBLEMATIC },
+    { "other", VERS__OTHER },
+    { "1.0", VERS_1_0 },
+    { "1.1", VERS_1_1 },
+    { "2.0", VERS_2_0 },
+    { "0.9", VERS_0_9 }
+};
+
+bool HttpRuleOptModule::parse_version_list(Value& v)
+{
+    v.set_first_token();
+    std::string tok;
+
+    while ( v.get_next_token(tok) )
+    {
+        if (tok[0] == '"')
+            tok.erase(0, 1);
+
+        if (tok.length() == 0)
+            continue;
+
+        if (tok[tok.length()-1] == '"')
+            tok.erase(tok.length()-1, 1);
+
+        auto iter = VersionStrToEnum.find(tok);
+        if (iter == VersionStrToEnum.end())
+        {
+            ParseError("Unrecognized version %s\n", tok.c_str());
+            return false;
+        }
+
+        para_list.version_flags[iter->second - VERS__MIN] = true;
+    }
+    return true;
+}
+
 bool HttpRuleOptModule::set(const char*, Value& v, SnortConfig*)
 {
     if (v.is("field"))
@@ -171,6 +210,10 @@ bool HttpRuleOptModule::set(const char*, Value& v, SnortConfig*)
     {
         return para_list.range.validate(v.get_string(), hdrs_num_range.c_str());
     }
+    else if (v.is("~version_list"))
+    {
+        return parse_version_list(v);
+    }
     return true;
 }
 
@@ -207,6 +250,7 @@ void HttpRuleOptModule::HttpRuleParaList::reset()
     query = false;
     fragment = false;
     range.init();
+    version_flags = 0;
 }
 
 uint32_t HttpIpsOption::hash() const
@@ -216,6 +260,7 @@ uint32_t HttpIpsOption::hash() const
     uint32_t c = buffer_info.hash();
     mix(a,b,c);
     a += range.hash();
+    b += (uint32_t)version_flags.to_ulong();
     finalize(a,b,c);
     return c;
 }
@@ -226,7 +271,8 @@ bool HttpIpsOption::operator==(const IpsOption& ips) const
     return IpsOption::operator==(ips) &&
            inspect_section == hio.inspect_section &&
            buffer_info == hio.buffer_info &&
-           range == hio.range;
+           range == hio.range &&
+           version_flags == hio.version_flags;
 }
 
 bool HttpIpsOption::retry(Cursor& current_cursor, const Cursor&)
@@ -241,6 +287,20 @@ bool HttpIpsOption::retry(Cursor& current_cursor, const Cursor&)
     return false;
 }
 
+IpsOption::EvalStatus HttpIpsOption::eval_version_match(Packet* p, const Http2FlowData* h2i_flow_data)
+{
+    const HttpFlowData* const flow_data = (h2i_flow_data != nullptr) ?
+        (HttpFlowData*)h2i_flow_data->get_hi_flow_data():
+        (HttpFlowData*)p->flow->get_flow_data(HttpFlowData::inspector_id);
+    const SourceId source_id = p->is_from_client() ? SRC_CLIENT : SRC_SERVER;
+    const VersionId version = flow_data->get_version_id(source_id);
+
+    if (version_flags[version - HttpEnums::VERS__MIN])
+        return MATCH;
+
+    return NO_MATCH;
+}
+
 IpsOption::EvalStatus HttpIpsOption::eval(Cursor& c, Packet* p)
 {
     RuleProfile profile(HttpRuleOptModule::http_ps[psi]);
@@ -262,7 +322,7 @@ IpsOption::EvalStatus HttpIpsOption::eval(Cursor& c, Packet* p)
     const HttpInspect* const hi = (h2i_flow_data != nullptr) ?
         (HttpInspect*)(p->flow->assistant_gadget) : (HttpInspect*)(p->flow->gadget);
 
-    if (buffer_info.type <= HTTP_BUFFER_MAX)
+    if (buffer_info.type <= HTTP__BUFFER_MAX)
     {
         const Field& http_buffer = hi->http_get_buf(c, p, buffer_info);
 
@@ -273,6 +333,10 @@ IpsOption::EvalStatus HttpIpsOption::eval(Cursor& c, Packet* p)
 
         return MATCH;
     }
+    else if (buffer_info.type == HTTP_VERSION_MATCH)
+    {
+        return eval_version_match(p, h2i_flow_data);
+    }
     else
     {
         const int32_t num_lines = hi->http_get_num_headers(p, buffer_info);
@@ -1353,6 +1417,52 @@ static const IpsApi num_trailers_api =
     nullptr
 };
 
+//-------------------------------------------------------------------------
+// http_version_match
+//-------------------------------------------------------------------------
+#undef IPS_OPT
+#define IPS_OPT "http_version_match"
+#undef IPS_HELP
+#define IPS_HELP "rule option to match version to listed values"
+
+static const Parameter version_match_params[] =
+{
+    { "~version_list", Parameter::PT_STRING, nullptr, nullptr,
+        "space-separated list of versions to match" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+static Module* version_match_mod_ctor()
+{
+    return new HttpRuleOptModule(IPS_OPT, IPS_HELP, HTTP_VERSION_MATCH, CAT_SET_OTHER,
+        PSI_VERSION_MATCH, version_match_params);
+}
+
+static const IpsApi version_match_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        1,
+        API_RESERVED,
+        API_OPTIONS,
+        IPS_OPT,
+        IPS_HELP,
+        version_match_mod_ctor,
+        HttpRuleOptModule::mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    HttpIpsOption::opt_ctor,
+    HttpIpsOption::opt_dtor,
+    nullptr
+};
+
 //-------------------------------------------------------------------------
 // plugins
 //-------------------------------------------------------------------------
@@ -1377,5 +1487,5 @@ const BaseApi* ips_http_trailer = &trailer_api.base;
 const BaseApi* ips_http_true_ip = &true_ip_api.base;
 const BaseApi* ips_http_uri = &uri_api.base;
 const BaseApi* ips_http_version = &version_api.base;
+const BaseApi* ips_http_version_match = &version_match_api.base;
 const BaseApi* ips_js_data = &js_data_api.base;
-
index 46e30e16f8799559ce468177ed4f60753618ec38..eda137cbfd6836d24a7e0de79ba1fce1c268476f 100644 (file)
 #include "http_enum.h"
 
 class HttpInspect;
+class Http2FlowData;
 
 enum PsIdx { PSI_CLIENT_BODY, PSI_COOKIE, PSI_HEADER, PSI_METHOD, PSI_PARAM,
     PSI_RAW_BODY, PSI_RAW_COOKIE, PSI_RAW_HEADER, PSI_RAW_REQUEST, PSI_RAW_STATUS,
     PSI_RAW_TRAILER, PSI_RAW_URI, PSI_STAT_CODE, PSI_STAT_MSG, PSI_TRAILER,
     PSI_TRUE_IP, PSI_URI, PSI_VERSION, PSI_JS_DATA, PSI_VBA_DATA,
-    PSI_RANGE_NUM_HDRS, PSI_RANGE_NUM_TRAILERS, PSI_MAX };
+    PSI_RANGE_NUM_HDRS, PSI_RANGE_NUM_TRAILERS, PSI_VERSION_MATCH, PSI_MAX };
 
 class HttpRuleOptModule : public snort::Module
 {
@@ -61,6 +62,7 @@ public:
 private:
     friend class HttpIpsOption;
     static THREAD_LOCAL std::array<snort::ProfileStats, PsIdx::PSI_MAX> http_ps;
+    static const int version_size = HttpEnums::VERS__MAX - HttpEnums::VERS__MIN + 1;
 
     struct HttpRuleParaList
     {
@@ -79,6 +81,7 @@ private:
         bool query;
         bool fragment;
         snort::RangeCheck range;
+        std::bitset<version_size> version_flags;
 
         void reset();
     };
@@ -92,6 +95,8 @@ private:
     HttpEnums::InspectSection inspect_section;
     uint64_t sub_id;
     uint64_t form;
+
+    bool parse_version_list(snort::Value& v);
 };
 
 class HttpIpsOption : public snort::IpsOption
@@ -102,7 +107,8 @@ public:
         key(cm->key), cat(cm->cat), psi(cm->psi),
         inspect_section(cm->inspect_section),
         buffer_info(cm->rule_opt_index, cm->sub_id, cm->form,
-        cm->para_list.param, cm->para_list.nocase), range(cm->para_list.range){}
+        cm->para_list.param, cm->para_list.nocase), range(cm->para_list.range),
+        version_flags(cm->para_list.version_flags){}
     snort::CursorActionType get_cursor_type() const override { return cat; }
     EvalStatus eval(Cursor&, snort::Packet*) override;
     uint32_t hash() const override;
@@ -119,6 +125,9 @@ private:
     const HttpEnums::InspectSection inspect_section;
     HttpBufferInfo buffer_info;
     const snort::RangeCheck range;
+    const std::bitset<HttpRuleOptModule::version_size> version_flags;
+
+    IpsOption::EvalStatus eval_version_match(snort::Packet* p, const Http2FlowData* h2i_flow_data);
 };
 
 #endif