diff --git a/gennghttpxfun.py b/gennghttpxfun.py index 496f1875..005f2875 100755 --- a/gennghttpxfun.py +++ b/gennghttpxfun.py @@ -107,7 +107,9 @@ OPTIONS = [ "add-forwarded", "strip-incoming-forwarded", "forwarded-by", - "forwarded-for" + "forwarded-for", + "response-header-field-buffer", + "max-response-header-fields" ] LOGVARS = [ diff --git a/src/shrpx.cc b/src/shrpx.cc index 9a08dab9..fe141ef2 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -1073,6 +1073,8 @@ void fill_default_config() { httpconf.no_host_rewrite = true; httpconf.header_field_buffer = 64_k; httpconf.max_header_fields = 100; + httpconf.response_header_field_buffer = 64_k; + httpconf.max_response_header_fields = 500; auto &http2conf = mod_config()->http2; { @@ -1772,14 +1774,28 @@ HTTP: --header-field-buffer= Set maximum buffer size for incoming HTTP request header field list. This is the sum of header name and value in - bytes. + bytes. If trailer fields exist, they are counted + towards this number. Default: )" << util::utos_unit(get_config()->http.header_field_buffer) << R"( --max-header-fields= Set maximum number of incoming HTTP request header - fields, which appear in one request or response header - field list. + fields. If trailer fields exist, they are counted + towards this number. Default: )" << get_config()->http.max_header_fields << R"( + --response-header-field-buffer= + Set maximum buffer size for incoming HTTP response + header field list. This is the sum of header name and + value in bytes. If trailer fields exist, they are + counted towards this number. + Default: )" + << util::utos_unit(get_config()->http.response_header_field_buffer) << R"( + --max-response-header-fields= + Set maximum number of incoming HTTP response header + fields. If trailer fields exist, they are counted + towards this number. + Default: )" << get_config()->http.max_response_header_fields + << R"( Debug: --frontend-http2-dump-request-header= @@ -2349,6 +2365,8 @@ int main(int argc, char **argv) { {SHRPX_OPT_STRIP_INCOMING_FORWARDED, no_argument, &flag, 98}, {SHRPX_OPT_FORWARDED_BY, required_argument, &flag, 99}, {SHRPX_OPT_FORWARDED_FOR, required_argument, &flag, 100}, + {SHRPX_OPT_RESPONSE_HEADER_FIELD_BUFFER, required_argument, &flag, 101}, + {SHRPX_OPT_MAX_RESPONSE_HEADER_FIELDS, required_argument, &flag, 102}, {nullptr, 0, nullptr, 0}}; int option_index = 0; @@ -2778,6 +2796,14 @@ int main(int argc, char **argv) { // --forwarded-for cmdcfgs.emplace_back(SHRPX_OPT_FORWARDED_FOR, optarg); break; + case 101: + // --response-header-field-buffer + cmdcfgs.emplace_back(SHRPX_OPT_RESPONSE_HEADER_FIELD_BUFFER, optarg); + break; + case 102: + // --max-response-header-fields + cmdcfgs.emplace_back(SHRPX_OPT_MAX_RESPONSE_HEADER_FIELDS, optarg); + break; default: break; } diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index eb915f01..a99e5eac 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -726,6 +726,7 @@ enum { SHRPX_OPTID_LISTENER_DISABLE_TIMEOUT, SHRPX_OPTID_LOG_LEVEL, SHRPX_OPTID_MAX_HEADER_FIELDS, + SHRPX_OPTID_MAX_RESPONSE_HEADER_FIELDS, SHRPX_OPTID_MRUBY_FILE, SHRPX_OPTID_NO_HOST_REWRITE, SHRPX_OPTID_NO_LOCATION_REWRITE, @@ -740,6 +741,7 @@ enum { SHRPX_OPTID_PRIVATE_KEY_PASSWD_FILE, SHRPX_OPTID_READ_BURST, SHRPX_OPTID_READ_RATE, + SHRPX_OPTID_RESPONSE_HEADER_FIELD_BUFFER, SHRPX_OPTID_RLIMIT_NOFILE, SHRPX_OPTID_STREAM_READ_TIMEOUT, SHRPX_OPTID_STREAM_WRITE_TIMEOUT, @@ -1258,6 +1260,9 @@ int option_lookup_token(const char *name, size_t namelen) { if (util::strieq_l("frontend-http2-window-bit", name, 25)) { return SHRPX_OPTID_FRONTEND_HTTP2_WINDOW_BITS; } + if (util::strieq_l("max-response-header-field", name, 25)) { + return SHRPX_OPTID_MAX_RESPONSE_HEADER_FIELDS; + } break; case 't': if (util::strieq_l("backend-keep-alive-timeou", name, 25)) { @@ -1292,6 +1297,11 @@ int option_lookup_token(const char *name, size_t namelen) { return SHRPX_OPTID_TLS_DYN_REC_WARMUP_THRESHOLD; } break; + case 'r': + if (util::strieq_l("response-header-field-buffe", name, 27)) { + return SHRPX_OPTID_RESPONSE_HEADER_FIELD_BUFFER; + } + break; case 's': if (util::strieq_l("http2-max-concurrent-stream", name, 27)) { return SHRPX_OPTID_HTTP2_MAX_CONCURRENT_STREAMS; @@ -2007,6 +2017,12 @@ int parse_config(const char *opt, const char *optarg, optarg); case SHRPX_OPTID_MAX_HEADER_FIELDS: return parse_uint(&mod_config()->http.max_header_fields, opt, optarg); + case SHRPX_OPTID_RESPONSE_HEADER_FIELD_BUFFER: + return parse_uint_with_unit( + &mod_config()->http.response_header_field_buffer, opt, optarg); + case SHRPX_OPTID_MAX_RESPONSE_HEADER_FIELDS: + return parse_uint(&mod_config()->http.max_response_header_fields, opt, + optarg); case SHRPX_OPTID_INCLUDE: { if (included_set.count(optarg)) { LOG(ERROR) << opt << ": " << optarg << " has already been included"; diff --git a/src/shrpx_config.h b/src/shrpx_config.h index 07c0fbb7..134b2518 100644 --- a/src/shrpx_config.h +++ b/src/shrpx_config.h @@ -196,6 +196,10 @@ constexpr char SHRPX_OPT_STRIP_INCOMING_FORWARDED[] = "strip-incoming-forwarded"; constexpr static char SHRPX_OPT_FORWARDED_BY[] = "forwarded-by"; constexpr char SHRPX_OPT_FORWARDED_FOR[] = "forwarded-for"; +constexpr char SHRPX_OPT_RESPONSE_HEADER_FIELD_BUFFER[] = + "response-header-field-buffer"; +constexpr char SHRPX_OPT_MAX_RESPONSE_HEADER_FIELDS[] = + "max-response-header-fields"; constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8; @@ -420,6 +424,8 @@ struct HttpConfig { StringRef server_name; size_t header_field_buffer; size_t max_header_fields; + size_t response_header_field_buffer; + size_t max_response_header_fields; bool no_via; bool no_location_rewrite; bool no_host_rewrite; diff --git a/src/shrpx_http2_session.cc b/src/shrpx_http2_session.cc index a49c799f..6b5625e3 100644 --- a/src/shrpx_http2_session.cc +++ b/src/shrpx_http2_session.cc @@ -729,12 +729,31 @@ int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, } auto &resp = downstream->response(); + auto &httpconf = get_config()->http; switch (frame->hd.type) { case NGHTTP2_HEADERS: { auto trailer = frame->headers.cat == NGHTTP2_HCAT_HEADERS && !downstream->get_expect_final_response(); + if (resp.fs.buffer_size() + namelen + valuelen > + httpconf.response_header_field_buffer || + resp.fs.num_fields() >= httpconf.max_response_header_fields) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) << "Too large or many header field size=" + << resp.fs.buffer_size() + namelen + valuelen + << ", num=" << resp.fs.num_fields() + 1; + } + + if (trailer) { + // We don't care trailer part exceeds header size limit; just + // discard it. + return 0; + } + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + if (trailer) { // just store header fields for trailer part resp.fs.add_trailer(name, namelen, value, valuelen, @@ -763,6 +782,20 @@ int on_header_callback(nghttp2_session *session, const nghttp2_frame *frame, auto &promised_req = promised_downstream->request(); + // We use request header limit for PUSH_PROMISE + if (promised_req.fs.buffer_size() + namelen + valuelen > + httpconf.header_field_buffer || + promised_req.fs.num_fields() >= httpconf.max_header_fields) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) + << "Too large or many header field size=" + << promised_req.fs.buffer_size() + namelen + valuelen + << ", num=" << promised_req.fs.num_fields() + 1; + } + + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + auto token = http2::lookup_token(name, namelen); promised_req.fs.add_header(name, namelen, value, valuelen, flags & NGHTTP2_NV_FLAG_NO_INDEX, token); diff --git a/src/shrpx_http_downstream_connection.cc b/src/shrpx_http_downstream_connection.cc index 982eedd7..2c39fe50 100644 --- a/src/shrpx_http_downstream_connection.cc +++ b/src/shrpx_http_downstream_connection.cc @@ -605,15 +605,57 @@ int htp_hdrs_completecb(http_parser *htp) { } } // namespace +namespace { +int ensure_header_field_buffer(const Downstream *downstream, + const HttpConfig &httpconf, size_t len) { + auto &resp = downstream->response(); + + if (resp.fs.buffer_size() + len > httpconf.response_header_field_buffer) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) << "Too large header header field size=" + << resp.fs.buffer_size() + len; + } + return -1; + } + + return 0; +} +} // namespace + +namespace { +int ensure_max_header_fields(const Downstream *downstream, + const HttpConfig &httpconf) { + auto &resp = downstream->response(); + + if (resp.fs.num_fields() >= httpconf.max_response_header_fields) { + if (LOG_ENABLED(INFO)) { + DLOG(INFO, downstream) + << "Too many header field num=" << resp.fs.num_fields() + 1; + } + return -1; + } + + return 0; +} +} // namespace + namespace { int htp_hdr_keycb(http_parser *htp, const char *data, size_t len) { auto downstream = static_cast(htp->data); auto &resp = downstream->response(); + auto &httpconf = get_config()->http; + + if (ensure_header_field_buffer(downstream, httpconf, len) != 0) { + return -1; + } if (downstream->get_response_state() == Downstream::INITIAL) { if (resp.fs.header_key_prev()) { resp.fs.append_last_header_key(data, len); } else { + if (ensure_max_header_fields(downstream, httpconf) != 0) { + return -1; + } resp.fs.add_header(std::string(data, len), ""); } } else { @@ -621,6 +663,12 @@ int htp_hdr_keycb(http_parser *htp, const char *data, size_t len) { if (resp.fs.trailer_key_prev()) { resp.fs.append_last_trailer_key(data, len); } else { + if (ensure_max_header_fields(downstream, httpconf) != 0) { + // Could not ignore this trailer field easily, since we may + // get its value in htp_hdr_valcb, and it will be added to + // wrong place or crash if trailer fields are currently empty. + return -1; + } resp.fs.add_trailer(std::string(data, len), ""); } } @@ -632,6 +680,11 @@ namespace { int htp_hdr_valcb(http_parser *htp, const char *data, size_t len) { auto downstream = static_cast(htp->data); auto &resp = downstream->response(); + auto &httpconf = get_config()->http; + + if (ensure_header_field_buffer(downstream, httpconf, len) != 0) { + return -1; + } if (downstream->get_response_state() == Downstream::INITIAL) { resp.fs.append_last_header_value(data, len);