diff --git a/CHANGES b/CHANGES
index d6d94d4ea6..16b30355e2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,19 @@
+6.1.0-dev.358 | 2023-09-01 11:53:55 +0200
+
+ * GH-1705: http: Prevent request/response de-synchronization and unbounded state growth (Arne Welzel, Corelight)
+
+ When http_reply events are received before http_request events, either
+ through faking traffic or possible re-ordering, it is possible to trigger
+ unbounded state growth due to later http_requests never being matched
+ again with responses.
+
+ Prevent this by synchronizing request/response counters when late
+ requests come in.
+
+ Also forcefully flush pending requests when http_replies are never
+ observed either due to the analyzer having been disabled or because
+ half-duplex traffic.
+
6.1.0-dev.356 | 2023-08-30 11:24:51 +0200
* testing/external: Exclude telemetry.log from baselines (Arne Welzel, Corelight)
diff --git a/NEWS b/NEWS
index 4b92775fe1..6d01ded839 100644
--- a/NEWS
+++ b/NEWS
@@ -82,6 +82,11 @@ Changed Functionality
anymore. Previously, broadcast packets originating from a likely server port
resulted in 255.255.255.255 being the originator in ``conn.log``.
+- When too many HTTP requests are pending, Zeek will now log them at once and
+ reset request/response correlation instead of running into unbounded state
+ growth. This behavior is configurable via a new option ``HTTP::max_pending_requests``.
+ The default is ``100``.
+
Removed Functionality
---------------------
diff --git a/VERSION b/VERSION
index 7172f4cd70..4c1fd50ab9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-6.1.0-dev.356
+6.1.0-dev.358
diff --git a/scripts/base/frameworks/notice/weird.zeek b/scripts/base/frameworks/notice/weird.zeek
index 0511fc1e22..54527a2232 100644
--- a/scripts/base/frameworks/notice/weird.zeek
+++ b/scripts/base/frameworks/notice/weird.zeek
@@ -144,7 +144,9 @@ export {
["FTP_password_too_long"] = ACTION_LOG_PER_CONN,
["HTTP_bad_chunk_size"] = ACTION_LOG,
["HTTP_chunked_transfer_for_multipart_message"] = ACTION_LOG,
+ ["HTTP_excessive_pipelining"] = ACTION_LOG,
["HTTP_overlapping_messages"] = ACTION_LOG,
+ ["HTTP_response_before_request"] = ACTION_LOG,
["unknown_HTTP_method"] = ACTION_LOG,
["HTTP_version_mismatch"] = ACTION_LOG,
["ident_request_addendum"] = ACTION_LOG,
diff --git a/scripts/base/protocols/http/main.zeek b/scripts/base/protocols/http/main.zeek
index e0f0cdc0a0..0f65c6b984 100644
--- a/scripts/base/protocols/http/main.zeek
+++ b/scripts/base/protocols/http/main.zeek
@@ -133,6 +133,12 @@ export {
## HTTP finalization hook. Remaining HTTP info may get logged when it's called.
global finalize_http: Conn::RemovalHook;
+
+ ## Only allow that many pending requests on a single connection.
+ ## If this number is exceeded, all pending requests are flushed
+ ## out and request/response tracking reset to prevent unbounded
+ ## state growth.
+ option max_pending_requests = 100;
}
# Add the http state tracking fields to the connection record.
@@ -205,6 +211,47 @@ event http_request(c: connection, method: string, original_URI: string,
Conn::register_removal_hook(c, finalize_http);
}
+ # Request/response tracking exists to account for HTTP pipelining.
+ # It fails if more responses have been seen than requests. If that
+ # happens, just fast-forward current_request such that the next
+ # response matches the in-flight request.
+ if ( c$http_state$current_request < c$http_state$current_response )
+ {
+ Reporter::conn_weird("HTTP_response_before_request", c);
+ c$http_state$current_request = c$http_state$current_response;
+ }
+
+ # Too many requests are pending for which we have not yet observed a
+ # reply. This might be due to excessive HTTP pipelining, one-sided
+ # traffic capture, or the responder side of the HTTP analyzer having
+ # been disabled. In any case, we simply log out all pending requests
+ # to make room for a new one. Any matching of pipelined requests and
+ # responses is most likely totally off anyhow.
+ if ( max_pending_requests > 0 && |c$http_state$pending| > max_pending_requests )
+ {
+ Reporter::conn_weird("HTTP_excessive_pipelining", c);
+
+ if ( c$http_state$current_response == 0 )
+ ++c$http_state$current_response;
+
+ while ( c$http_state$current_response < c$http_state$current_request )
+ {
+ local cr = c$http_state$current_response;
+ if ( cr in c$http_state$pending )
+ {
+ Log::write(HTTP::LOG, c$http_state$pending[cr]);
+ delete c$http_state$pending[cr];
+ }
+ else
+ {
+ # The above should have been true...
+ # Reporter::error(fmt("Expected pending request at %d", cr));
+ }
+
+ ++c$http_state$current_response;
+ }
+ }
+
++c$http_state$current_request;
set_state(c, T);
diff --git a/testing/btest/Baseline/plugins.hooks/output b/testing/btest/Baseline/plugins.hooks/output
index 1acce5b4a1..ee21bb2757 100644
--- a/testing/btest/Baseline/plugins.hooks/output
+++ b/testing/btest/Baseline/plugins.hooks/output
@@ -559,6 +559,7 @@
0.000000 MetaHookPost CallFunction(Option::set_change_handler, , (HTTP::http_methods, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)) ->
0.000000 MetaHookPost CallFunction(Option::set_change_handler, , (HTTP::max_files_orig, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)) ->
0.000000 MetaHookPost CallFunction(Option::set_change_handler, , (HTTP::max_files_resp, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)) ->
+0.000000 MetaHookPost CallFunction(Option::set_change_handler, , (HTTP::max_pending_requests, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)) ->
0.000000 MetaHookPost CallFunction(Option::set_change_handler, , (HTTP::proxy_headers, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)) ->
0.000000 MetaHookPost CallFunction(Option::set_change_handler, , (Input::default_mode, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)) ->
0.000000 MetaHookPost CallFunction(Option::set_change_handler, , (Input::default_reader, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)) ->
@@ -2185,6 +2186,7 @@
0.000000 MetaHookPre CallFunction(Option::set_change_handler, , (HTTP::http_methods, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100))
0.000000 MetaHookPre CallFunction(Option::set_change_handler, , (HTTP::max_files_orig, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100))
0.000000 MetaHookPre CallFunction(Option::set_change_handler, , (HTTP::max_files_resp, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100))
+0.000000 MetaHookPre CallFunction(Option::set_change_handler, , (HTTP::max_pending_requests, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100))
0.000000 MetaHookPre CallFunction(Option::set_change_handler, , (HTTP::proxy_headers, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100))
0.000000 MetaHookPre CallFunction(Option::set_change_handler, , (Input::default_mode, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100))
0.000000 MetaHookPre CallFunction(Option::set_change_handler, , (Input::default_reader, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100))
@@ -3810,6 +3812,7 @@
0.000000 | HookCallFunction Option::set_change_handler(HTTP::http_methods, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)
0.000000 | HookCallFunction Option::set_change_handler(HTTP::max_files_orig, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)
0.000000 | HookCallFunction Option::set_change_handler(HTTP::max_files_resp, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)
+0.000000 | HookCallFunction Option::set_change_handler(HTTP::max_pending_requests, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)
0.000000 | HookCallFunction Option::set_change_handler(HTTP::proxy_headers, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)
0.000000 | HookCallFunction Option::set_change_handler(Input::default_mode, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)
0.000000 | HookCallFunction Option::set_change_handler(Input::default_reader, Config::config_option_changed{ if ( == Config::location) return (Config::new_value)Config::log = Config::Info($ts=network_time(), $id=Config::ID, $old_value=Config::format_value(lookup_ID(Config::ID)), $new_value=Config::format_value(Config::new_value))if ( != Config::location) Config::log$location = Config::locationLog::write(Config::LOG, to_any_coerceConfig::log)return (Config::new_value)}, -100)
diff --git a/testing/btest/Baseline/scripts.base.protocols.http.http-09-content-length-confusion/http.log b/testing/btest/Baseline/scripts.base.protocols.http.http-09-content-length-confusion/http.log
index 9bca082c7f..36fce1ce54 100644
--- a/testing/btest/Baseline/scripts.base.protocols.http.http-09-content-length-confusion/http.log
+++ b/testing/btest/Baseline/scripts.base.protocols.http.http-09-content-length-confusion/http.log
@@ -7,6 +7,6 @@
#open XXXX-XX-XX-XX-XX-XX
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p trans_depth method host uri referrer version user_agent origin request_body_len response_body_len status_code status_msg info_code info_msg tags username password proxied orig_fuids orig_filenames orig_mime_types resp_fuids resp_filenames resp_mime_types
#types time string addr port addr port count string string string string string string string count count count string count string set[enum] string string set[string] vector[string] vector[string] vector[string] vector[string] vector[string] vector[string]
-XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 46982 127.0.0.1 80 1 GET - / - 1.1 - - 0 0 200 OK - - (empty) - - - - - - - - -
-XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 46982 127.0.0.1 80 2 - - - - 0.9 - - 0 19 0 - - (empty) - - - - - - F7s8014qOBt9Gyy3w7 - text/plain
+XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 46982 127.0.0.1 80 1 - - - - 1.1 - - 0 0 200 OK - - (empty) - - - - - - - - -
+XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 46982 127.0.0.1 80 2 GET - / - 0.9 - - 0 19 0 - - (empty) - - - - - - F7s8014qOBt9Gyy3w7 - text/plain
#close XXXX-XX-XX-XX-XX-XX
diff --git a/testing/btest/Baseline/scripts.base.protocols.http.http-09-content-length-confusion/weird.log b/testing/btest/Baseline/scripts.base.protocols.http.http-09-content-length-confusion/weird.log
index b6c643871a..1e04819b2a 100644
--- a/testing/btest/Baseline/scripts.base.protocols.http.http-09-content-length-confusion/weird.log
+++ b/testing/btest/Baseline/scripts.base.protocols.http.http-09-content-length-confusion/weird.log
@@ -8,5 +8,6 @@
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p name addl notice peer source
#types time string addr port addr port string string bool string string
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 46982 127.0.0.1 80 http_09_reply_before_request - F zeek HTTP
+XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 46982 127.0.0.1 80 HTTP_response_before_request - F zeek -
XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 46982 127.0.0.1 80 HTTP_version_mismatch - F zeek HTTP
#close XXXX-XX-XX-XX-XX-XX
diff --git a/testing/btest/Baseline/scripts.base.protocols.http.http-desynched/http.log b/testing/btest/Baseline/scripts.base.protocols.http.http-desynched/http.log
new file mode 100644
index 0000000000..c60072ec1d
--- /dev/null
+++ b/testing/btest/Baseline/scripts.base.protocols.http.http-desynched/http.log
@@ -0,0 +1,17 @@
+### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
+#separator \x09
+#set_separator ,
+#empty_field (empty)
+#unset_field -
+#path http
+#open XXXX-XX-XX-XX-XX-XX
+#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p trans_depth method host uri referrer version user_agent origin request_body_len response_body_len status_code status_msg info_code info_msg tags username password proxied orig_fuids orig_filenames orig_mime_types resp_fuids resp_filenames resp_mime_types
+#types time string addr port addr port count string string string string string string string count count count string count string set[enum] string string set[string] vector[string] vector[string] vector[string] vector[string] vector[string] vector[string]
+XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 50170 127.0.0.1 8080 1 GET localhost:8080 / - 1.1 python-requests/2.31.0 - 0 19 200 OK - - (empty) - - - - - - FlUDto3MxYxQV9pVnf - text/html
+XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 50170 127.0.0.1 8080 2 - - - - 1.1 - - 0 19 200 OK - - (empty) - - - - - - F7ynSw2U0QnJcw9Qth - text/html
+XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 50170 127.0.0.1 8080 3 - - - - 1.1 - - 0 19 200 OK - - (empty) - - - - - - FeY8eU1T8QwTaI0456 - text/html
+XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 50170 127.0.0.1 8080 4 GET localhost:8080 / - 1.1 python-requests/2.31.0 - 0 19 200 OK - - (empty) - - - - - - FfzjgM2OnUfAX9LnAd - text/html
+XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 50170 127.0.0.1 8080 5 GET localhost:8080 / - 1.1 python-requests/2.31.0 - 0 19 200 OK - - (empty) - - - - - - FtPJJF1xSxzd8blV4g - text/html
+XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 50170 127.0.0.1 8080 6 GET localhost:8080 / - 1.1 python-requests/2.31.0 - 0 19 200 OK - - (empty) - - - - - - FkCd8G2tsvSm17YOj6 - text/html
+XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 50170 127.0.0.1 8080 7 GET localhost:8080 / - 1.1 python-requests/2.31.0 - 0 19 200 OK - - (empty) - - - - - - FSa22J3bRk1Qv0hkE1 - text/html
+#close XXXX-XX-XX-XX-XX-XX
diff --git a/testing/btest/Baseline/scripts.base.protocols.http.http-pending-state-growth/out b/testing/btest/Baseline/scripts.base.protocols.http.http-pending-state-growth/out
new file mode 100644
index 0000000000..c3e974e1b2
--- /dev/null
+++ b/testing/btest/Baseline/scripts.base.protocols.http.http-pending-state-growth/out
@@ -0,0 +1,4 @@
+### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
+http_state pending, 100
+total http.log lines
+1000
diff --git a/testing/btest/Baseline/scripts.base.protocols.http.http-pending-state-growth/weird.log b/testing/btest/Baseline/scripts.base.protocols.http.http-pending-state-growth/weird.log
new file mode 100644
index 0000000000..0bdb746ff0
--- /dev/null
+++ b/testing/btest/Baseline/scripts.base.protocols.http.http-pending-state-growth/weird.log
@@ -0,0 +1,11 @@
+### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
+#separator \x09
+#set_separator ,
+#empty_field (empty)
+#unset_field -
+#path weird
+#open XXXX-XX-XX-XX-XX-XX
+#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p name addl notice peer source
+#types time string addr port addr port string string bool string string
+XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ::1 44730 ::1 80 HTTP_excessive_pipelining - F zeek -
+#close XXXX-XX-XX-XX-XX-XX
diff --git a/testing/btest/Traces/http/1000-requests-one-dropped-response.pcap.gz b/testing/btest/Traces/http/1000-requests-one-dropped-response.pcap.gz
new file mode 100644
index 0000000000..6acd83b874
Binary files /dev/null and b/testing/btest/Traces/http/1000-requests-one-dropped-response.pcap.gz differ
diff --git a/testing/btest/Traces/http/http-desync-request-response-5.pcap b/testing/btest/Traces/http/http-desync-request-response-5.pcap
new file mode 100644
index 0000000000..5ff7821af2
Binary files /dev/null and b/testing/btest/Traces/http/http-desync-request-response-5.pcap differ
diff --git a/testing/btest/scripts/base/protocols/http/http-desynched.zeek b/testing/btest/scripts/base/protocols/http/http-desynched.zeek
new file mode 100644
index 0000000000..3661ed5085
--- /dev/null
+++ b/testing/btest/scripts/base/protocols/http/http-desynched.zeek
@@ -0,0 +1,13 @@
+# @TEST-DOC: 5 HTTP requests, the first one is responded to with 3 HTTP responses.
+#
+# @TEST-EXEC: zeek -b -r $TRACES/http/http-desync-request-response-5.pcap %INPUT
+# @TEST-EXEC: btest-diff http.log
+
+@load base/protocols/http
+
+# mime type is irrelevant to this test, so filter it out
+event zeek_init()
+ {
+ Log::remove_default_filter(HTTP::LOG);
+ Log::add_filter(HTTP::LOG, [$name="less-mime-types", $exclude=set("mime_type")]);
+ }
diff --git a/testing/btest/scripts/base/protocols/http/http-pending-state-growth.zeek b/testing/btest/scripts/base/protocols/http/http-pending-state-growth.zeek
new file mode 100644
index 0000000000..50afba1136
--- /dev/null
+++ b/testing/btest/scripts/base/protocols/http/http-pending-state-growth.zeek
@@ -0,0 +1,15 @@
+# @TEST-DOC: Pcap has a gap for the server side. This previously caused unbounded state growth in c$http_state$pending.
+#
+# @TEST-EXEC: zcat <$TRACES/http/1000-requests-one-dropped-response.pcap.gz | zeek -C -b -r - %INPUT >out
+# @TEST-EXEC: echo "total http.log lines" >>out
+# @TEST-EXEC: grep -v '^#' http.log | wc -l | sed 's/ //g' >>out
+# @TEST-EXEC: btest-diff out
+# @TEST-EXEC: btest-diff weird.log
+
+@load base/protocols/http
+
+event connection_state_remove(c: connection)
+ {
+ if ( c?$http_state )
+ print "http_state pending", |c$http_state$pending|;
+ }