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
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
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
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)
{}
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
ips_http_true_ip,
ips_http_uri,
ips_http_version,
+ ips_http_version_match,
ips_js_data,
nullptr
};
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
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 {
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,
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
};
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,
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
};
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;
}
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);
}
}
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();
{ 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" },
"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 }
};
// 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)
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:
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"))
{
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;
}
query = false;
fragment = false;
range.init();
+ version_flags = 0;
}
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;
}
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&)
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]);
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);
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);
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
//-------------------------------------------------------------------------
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;
-
#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
{
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
{
bool query;
bool fragment;
snort::RangeCheck range;
+ std::bitset<version_size> version_flags;
void reset();
};
HttpEnums::InspectSection inspect_section;
uint64_t sub_id;
uint64_t form;
+
+ bool parse_version_list(snort::Value& v);
};
class HttpIpsOption : public snort::IpsOption
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;
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