From ad95629941d7c41e2d86d46d4f591e1f416bd3d5 Mon Sep 17 00:00:00 2001 From: Roman Belozerov Date: Tue, 10 Mar 2026 16:55:37 +0400 Subject: [PATCH 1/7] add tests for CVE-2019-9517 and CVE-2019-9511 --- framework/deproxy/deproxy_base.py | 6 ++ tests/stress/test_slow_read.py | 162 ++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 tests/stress/test_slow_read.py diff --git a/framework/deproxy/deproxy_base.py b/framework/deproxy/deproxy_base.py index 8b3b0b874..6cf66df18 100755 --- a/framework/deproxy/deproxy_base.py +++ b/framework/deproxy/deproxy_base.py @@ -59,6 +59,12 @@ def set_rst_tcp_to_closing_connection(self) -> None: def set_lock(self, polling_lock: threading.Lock) -> None: self.__polling_lock = polling_lock + def set_size_of_receiving_buffer(self, new_buffer_size: int) -> None: + """Set the size of the receiving buffer.""" + self.__acquire() + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, new_buffer_size) + self.__release() + def _bind(self, address: tuple) -> None: """ Wrapper for `bind` method to add some log details. diff --git a/tests/stress/test_slow_read.py b/tests/stress/test_slow_read.py new file mode 100644 index 000000000..ddf9d9a04 --- /dev/null +++ b/tests/stress/test_slow_read.py @@ -0,0 +1,162 @@ +__author__ = "Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2026 Tempesta Technologies, Inc." +__license__ = "GPL2" + +from pathlib import Path + +from framework.helpers import remote, tf_cfg +from framework.test_suite import tester + + +class TestSlowRead(tester.TempestaTest): + clients = [ + { + "id": f"deproxy-{i}", + "type": "deproxy_h2", + "addr": "${tempesta_ip}", + "port": "443", + "ssl": True, + "ssl_hostname": "tempesta-tech.com", + } + for i in range(20) + ] + + backends = [ + { + "id": "nginx", + "type": "nginx", + "status_uri": "http://${server_ip}:8000/nginx_status", + "config": """ +pid ${pid}; +worker_processes auto; + +events { + worker_connections 1024; + use epoll; +} + +http { + keepalive_timeout ${server_keepalive_timeout}; + keepalive_requests 10; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + open_file_cache max=1000; + open_file_cache_valid 30s; + open_file_cache_min_uses 2; + open_file_cache_errors off; + + # [ debug | info | notice | warn | error | crit | alert | emerg ] + # Fully disable log errors. + error_log /dev/null emerg; + + # Disable access log altogether. + access_log off; + + server { + listen ${server_ip}:8000; + + location / { + root ${server_resources}; + } + location /nginx_status { + stub_status on; + } + } +} +""", + } + ] + + tempesta = { + "config": """ + cache 0; + keepalive_timeout 10; + listen 443 proto=h2; + + tls_match_any_server_name; + + srv_group default { + server ${server_ip}:8000; + } + + vhost tempesta-tech.com { + tls_certificate ${tempesta_workdir}/tempesta.crt; + tls_certificate_key ${tempesta_workdir}/tempesta.key; + proxy_pass default; + } + + """ + } + + response_file_name = "large.txt" + response_file_path = str(Path(tf_cfg.cfg.get("Server", "resources")) / response_file_name) + + @classmethod + def setUpClass(cls): + super().setUpClass() + remote.server.run_cmd( + f"fallocate -l {1024**2 * int(tf_cfg.cfg.get("General", "long_body_size"))} {cls.response_file_path}" + ) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + remote.server.remove_file(cls.response_file_path) + + def test_cve_2019_9511(self): + """ + CVE-2019-9511 - “Data Dribble” + Some HTTP/2 implementations are vulnerable to window size manipulation + and stream prioritization manipulation, potentially leading to a denial of service. + The attacker requests a large amount of data from a specified resource over multiple streams. + They manipulate window size and stream priority to force the server to queue the data in 1-byte chunks. + Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both. + """ + self.start_all_services() + + request = self.get_clients()[0].create_request( + method="GET", + headers=[], + authority="tempesta-tech.com", + uri=f"/{self.response_file_name}", + ) + + for client in self.get_clients(): + client.update_initial_settings(initial_window_size=1) + client.send_bytes(client.h2_connection.data_to_send()) + self.assertTrue(client.wait_for_ack_settings()) + client.make_requests([request] * 100) + + for client in self.get_clients(): + client.wait_for_connection_close(strict=True) + + def test_cve_2019_9517(self): + """ + CVE-2019-9517 - “Internal Data Buffering” + Some HTTP/2 implementations are vulnerable to unconstrained internal data buffering, + potentially leading to a denial of service. The attacker opens the HTTP/2 window + so the peer can send without constraint; however, they leave the TCP window closed + so the peer cannot actually write (many of) the bytes on the wire. + The attacker sends a stream of requests for a large response object. + Depending on how the servers queue the responses, this can consume excess memory, CPU, or both. + """ + self.start_all_services() + + request = self.get_clients()[0].create_request( + method="GET", + headers=[], + authority="tempesta-tech.com", + uri=f"/{self.response_file_name}", + ) + + for client in self.get_clients(): + client.update_initial_settings() + client.send_bytes(client.h2_connection.data_to_send()) + self.assertTrue(client.wait_for_ack_settings()) + client.set_size_of_receiving_buffer(new_buffer_size=1) + client.make_requests([request] * 100) + + for client in self.get_clients(): + client.wait_for_connection_close(timeout=20, strict=True) From 01d553a3cb2cb365f5db729087c08b6644e7b0d6 Mon Sep 17 00:00:00 2001 From: Roman Belozerov Date: Wed, 11 Mar 2026 12:43:34 +0400 Subject: [PATCH 2/7] move CVE tests to the separate directory and update tests_disabled* files --- tests/cve/__init__.py | 0 .../test_cve_2019.py} | 0 tests/tests_disabled.json | 8 +++++++ tests/tests_disabled_tcpseg.json | 24 ++++--------------- 4 files changed, 12 insertions(+), 20 deletions(-) create mode 100644 tests/cve/__init__.py rename tests/{stress/test_slow_read.py => cve/test_cve_2019.py} (100%) diff --git a/tests/cve/__init__.py b/tests/cve/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/stress/test_slow_read.py b/tests/cve/test_cve_2019.py similarity index 100% rename from tests/stress/test_slow_read.py rename to tests/cve/test_cve_2019.py diff --git a/tests/tests_disabled.json b/tests/tests_disabled.json index c1eb2b6af..804b2becf 100644 --- a/tests/tests_disabled.json +++ b/tests/tests_disabled.json @@ -204,6 +204,14 @@ { "name": "tests.server_connections.test_close_dead_connections.TestFinishH2StreamsByClient.test_drop_server_connection_for_goaway", "reason": "Disabled by issue #2626" + }, + { + "name": "tests.cve.test_cve_2019.TestSlowRead.test_cve_2019_9511", + "reason": "Disabled by issue #2627" + }, + { + "name": "tests.cve.test_cve_2019.TestSlowRead.test_cve_2019_9517", + "reason": "Disabled by issue #1715" } ] } diff --git a/tests/tests_disabled_tcpseg.json b/tests/tests_disabled_tcpseg.json index 3cb925dda..fd5d57d7e 100644 --- a/tests/tests_disabled_tcpseg.json +++ b/tests/tests_disabled_tcpseg.json @@ -1,6 +1,10 @@ { "disable" : true, "disabled" : [ + { + "name": "tests.cve", + "reason": "These tests should not be run with TCP segmentation." + }, { "name": "tests.nonidempotent.test_nonidempotent.RetryNonIdempotentPostH1Test", "reason": "These tests should not be run with TCP segmentation. It is difficult to ensure stability in this test." @@ -161,10 +165,6 @@ "name": "tests.http2_general.test_h2_hpack.TestHpackBomb", "reason": "These tests should not be run with TCP segmentation. We have separate tests for `http_max_header_list_size`." }, - { - "name": "tests.stress.test_ddos", - "reason": "These tests should not be run with TCP segmentation. These tests does not use deproxy." - }, { "name": "tests.frang.test_config", "reason": "These tests should not be run with TCP segmentation." @@ -256,22 +256,6 @@ { "name": "tests.frang.test_concurrent_connections.TestConcurrentConnectionsNonTempesta", "reason": "Is not intended to run with segmentation." - }, - { - "name": "tests.stress.test_stress.TestContinuationFlood", - "reason": "Disabled by test issue #817" - }, - { - "name": "tests.stress.test_stress.H2LoadStressMTU80", - "reason": "Disabled by test issue #817" - }, - { - "name": "tests.stress.test_stress.TlsWrkStressMTU80", - "reason": "Disabled by test issue #817" - }, - { - "name": "tests.stress.test_stress.WrkStressMTU80", - "reason": "Disabled by test issue #817" } ] } From 1afe33337253b29d57dcc764f28cda9e69391633 Mon Sep 17 00:00:00 2001 From: Roman Belozerov Date: Wed, 11 Mar 2026 14:00:21 +0400 Subject: [PATCH 3/7] update tests_retry. These tests were fixed earlier. --- tests/tests_retry | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/tests_retry b/tests/tests_retry index 44d48b1a3..e69de29bb 100644 --- a/tests/tests_retry +++ b/tests/tests_retry @@ -1,3 +0,0 @@ -tests.frang.test_request_rate_burst -tests.frang.test_http_resp_code_block -tests.leaks From 4793512113a91119c95a90f554b8f25513be06b9 Mon Sep 17 00:00:00 2001 From: Roman Belozerov Date: Wed, 11 Mar 2026 14:01:26 +0400 Subject: [PATCH 4/7] Move flood tests to CVE directory --- tests/cve/test_cve.py | 355 ++++++++++++++++++++++++++++ tests/cve/test_cve_2019.py | 162 ------------- tests/stress/test_stress.py | 206 ---------------- tests/tests_disabled.json | 12 +- tests/tests_disabled_dbgkernel.json | 2 +- tests/tests_disabled_remote.json | 10 +- 6 files changed, 363 insertions(+), 384 deletions(-) create mode 100644 tests/cve/test_cve.py delete mode 100644 tests/cve/test_cve_2019.py diff --git a/tests/cve/test_cve.py b/tests/cve/test_cve.py new file mode 100644 index 000000000..08efa61f8 --- /dev/null +++ b/tests/cve/test_cve.py @@ -0,0 +1,355 @@ +__author__ = "Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2026 Tempesta Technologies, Inc." +__license__ = "GPL2" + +from pathlib import Path + +from framework.deproxy.deproxy_message import HttpMessage +from framework.helpers import dmesg, remote, tf_cfg +from framework.test_suite import marks, tester + + +class TestSlowRead(tester.TempestaTest): + clients = [ + { + "id": f"deproxy-{i}", + "type": "deproxy_h2", + "addr": "${tempesta_ip}", + "port": "443", + "ssl": True, + "ssl_hostname": "tempesta-tech.com", + } + for i in range(20) + ] + + backends = [ + { + "id": "nginx", + "type": "nginx", + "status_uri": "http://${server_ip}:8000/nginx_status", + "config": """ +pid ${pid}; +worker_processes auto; + +events { + worker_connections 1024; + use epoll; +} + +http { + keepalive_timeout ${server_keepalive_timeout}; + keepalive_requests 10; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + open_file_cache max=1000; + open_file_cache_valid 30s; + open_file_cache_min_uses 2; + open_file_cache_errors off; + + # [ debug | info | notice | warn | error | crit | alert | emerg ] + # Fully disable log errors. + error_log /dev/null emerg; + + # Disable access log altogether. + access_log off; + + server { + listen ${server_ip}:8000; + + location / { + root ${server_resources}; + } + location /nginx_status { + stub_status on; + } + } +} +""", + } + ] + + tempesta = { + "config": """ + cache 0; + keepalive_timeout 10; + listen 443 proto=h2; + + tls_match_any_server_name; + + srv_group default { + server ${server_ip}:8000; + } + + vhost tempesta-tech.com { + tls_certificate ${tempesta_workdir}/tempesta.crt; + tls_certificate_key ${tempesta_workdir}/tempesta.key; + proxy_pass default; + } + + """ + } + + response_file_name = "large.txt" + response_file_path = str(Path(tf_cfg.cfg.get("Server", "resources")) / response_file_name) + + @classmethod + def setUpClass(cls): + super().setUpClass() + remote.server.run_cmd( + f"fallocate -l {1024**2 * int(tf_cfg.cfg.get("General", "long_body_size"))} {cls.response_file_path}" + ) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + remote.server.remove_file(cls.response_file_path) + + def test_cve_2019_9511(self): + """ + CVE-2019-9511 - “Data Dribble” + Some HTTP/2 implementations are vulnerable to window size manipulation + and stream prioritization manipulation, potentially leading to a denial of service. + The attacker requests a large amount of data from a specified resource over multiple streams. + They manipulate window size and stream priority to force the server to queue the data in 1-byte chunks. + Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both. + """ + self.start_all_services() + + request = self.get_clients()[0].create_request( + method="GET", + headers=[], + authority="tempesta-tech.com", + uri=f"/{self.response_file_name}", + ) + + for client in self.get_clients(): + client.update_initial_settings(initial_window_size=1) + client.send_bytes(client.h2_connection.data_to_send()) + self.assertTrue(client.wait_for_ack_settings()) + client.make_requests([request] * 100) + + for client in self.get_clients(): + client.wait_for_connection_close(strict=True) + + def test_cve_2019_9517(self): + """ + CVE-2019-9517 - “Internal Data Buffering” + Some HTTP/2 implementations are vulnerable to unconstrained internal data buffering, + potentially leading to a denial of service. The attacker opens the HTTP/2 window + so the peer can send without constraint; however, they leave the TCP window closed + so the peer cannot actually write (many of) the bytes on the wire. + The attacker sends a stream of requests for a large response object. + Depending on how the servers queue the responses, this can consume excess memory, CPU, or both. + """ + self.start_all_services() + + request = self.get_clients()[0].create_request( + method="GET", + headers=[], + authority="tempesta-tech.com", + uri=f"/{self.response_file_name}", + ) + + for client in self.get_clients(): + client.update_initial_settings() + client.send_bytes(client.h2_connection.data_to_send()) + self.assertTrue(client.wait_for_ack_settings()) + client.set_size_of_receiving_buffer(new_buffer_size=1) + client.make_requests([request] * 100) + + for client in self.get_clients(): + client.wait_for_connection_close(timeout=20, strict=True) + + +class TestHttp2FrameFlood(tester.TempestaTest): + """ + Test ability to handle requests from the client + under control frames flood. + Also check that there is no kernel BUGS and WARNINGs + under flood. + """ + + backends = [ + { + "id": "deproxy", + "type": "deproxy", + "port": "8000", + "response": "static", + "response_content": "HTTP/1.1 200 OK\r\nServer: Debian\r\nContent-Length: 0\r\n\r\n", + } + ] + + tempesta = { + "config": """ + listen 443 proto=h2; + + server ${server_ip}:8000; + + tls_certificate ${tempesta_workdir}/tempesta.crt; + tls_certificate_key ${tempesta_workdir}/tempesta.key; + tls_match_any_server_name; + cache 0; + """ + } + + clients = [ + { + "id": "ctrl_frames_flood", + "type": "external", + "binary": "ctrl_frames_flood", + "ssl": True, + "cmd_args": ( + "-address ${tempesta_ip}:443 -threads 4 -connections 100 -frame_count 100000" + ), + }, + { + "id": "gflood", + "type": "external", + "binary": "gflood", + "ssl": True, + "cmd_args": ( + "-address ${tempesta_ip}:443 -host tempesta-tech.com " + "-threads 4 -connections 10000 -streams 100 -headers_cnt 7" + ), + }, + ] + + @dmesg.limited_rate_on_tempesta_node + def test_cve_2024_2758(self): + """ + CVE-2024-2758 "Continuation flood" + Many HTTP/2 implementations do not properly limit or sanitize the amount of CONTINUATION frames sent within + a single stream. An attacker that can send packets to a target server can send a stream of CONTINUATION + frames that will not be appended to the header list in memory but will still be processed and decoded + by the server or will be appended to the header list, causing an out of memory (OOM) crash. + """ + self.start_all_services(client=False) + + client = self.get_client("gflood") + + client.start() + self.wait_while_busy(client) + client.stop() + + self.assertEqual(0, client.returncode) + + @marks.Parameterize.expand( + [ + marks.Param( + name="2019_9512", + cmd_args=f"-ctrl_frame_type ping_frame", + stat_name="cl_ping_frame_exceeded", + ), + marks.Param( + name="2019_9515", + cmd_args=f"-ctrl_frame_type settings_frame", + stat_name="cl_settings_frame_exceeded", + ), + marks.Param( + name="2023-44487", + cmd_args=f"-ctrl_frame_type rapid_reset -rapid_reset_type rst", + stat_name="cl_rst_frame_exceeded", + ), + marks.Param( + name="2025-8671_by_window_update", + cmd_args=f"-ctrl_frame_type window_update", + stat_name="cl_wnd_update_frame_exceeded", + ), + marks.Param( + name="2025-8671_by_window_update", + cmd_args=f"-ctrl_frame_type rapid_reset -rapid_reset_type window_update", + stat_name="cl_rst_frame_exceeded", + ), + marks.Param( + name="2025-8671_by_priority", + cmd_args=f"-ctrl_frame_type rapid_reset -rapid_reset_type priority", + stat_name="cl_rst_frame_exceeded", + ), + marks.Param( + name="2025-8671_by_flood_rst_batch", + cmd_args=f"-ctrl_frame_type rapid_reset -rapid_reset_type batch", + stat_name="cl_rst_frame_exceeded", + ), + marks.Param( + name="2025-8671_by_headers_max_concurrent_streams_exceeded", + cmd_args=f"-ctrl_frame_type rapid_reset -rapid_reset_type headers_by_max_streams_exceeded", + stat_name="cl_rst_frame_exceeded", + ), + marks.Param( + name="2025-8671_by_headers_invalid_dependency", + cmd_args=f"-ctrl_frame_type rapid_reset -rapid_reset_type headers_by_invalid_dependency", + stat_name="cl_rst_frame_exceeded", + ), + marks.Param( + name="2025-8671_by_incorrect_frame_type", + cmd_args=f"-ctrl_frame_type rapid_reset -rapid_reset_type incorrect_frame_type", + stat_name="cl_rst_frame_exceeded", + ), + marks.Param( + name="2025-8671_by_incorrect_header", + cmd_args=f"-ctrl_frame_type rapid_reset -rapid_reset_type incorrect_header", + stat_name="cl_rst_frame_exceeded", + ), + ] + ) + @dmesg.unlimited_rate_on_tempesta_node + def test_cve(self, name, cmd_args, stat_name): + """ + CVE-2019-9512 “Ping Flood” + Some HTTP/2 implementations are vulnerable to ping floods, potentially leading to a denial of service. + The attacker sends continual pings to an HTTP/2 peer, causing the peer to build an internal queue of responses. + Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both. + + CVE-2019-9514 "Reset Flood" + Some HTTP/2 implementations are vulnerable to a reset flood, potentially leading to a denial of service. + Servers that accept direct connections from untrusted clients could be remotely made to allocate an + unlimited amount of memory, until the program crashes. + The attacker opens a number of streams and sends an invalid request over each stream that should solicit + a stream of RST_STREAM frames from the peer. Depending on how the peer queues the RST_STREAM frames, + this can consume excess memory, CPU, or both. + + CVE-2019-9515 "Settings Flood" + Some HTTP/2 implementations are vulnerable to a settings flood, potentially leading to a denial of service. + The attacker sends a stream of SETTINGS frames to the peer. Since the RFC requires that the peer reply with + one acknowledgement per SETTINGS frame, an empty SETTINGS frame is almost equivalent in behavior to a ping. + Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both. + + CVE-2023-44487 "Rapid Reset" + The HTTP/2 protocol allows clients to indicate to the server that a previous stream should be canceled + by sending RST_STREAM frame. The protocol does not require the client and server to coordinate + the cancellation in any way, the client may do it unilaterally. The client may also assume that + the cancellation will take effect immediately when the server receives the RST_STREAM frame, + before any other data from that TCP connection is processed. + + CVE-2025-8671 "Made You Reset" + By opening streams and then rapidly triggering the server to reset them using malformed frames or flow + control errors, an attacker can exploit a discrepancy created between HTTP/2 streams accounting and the + servers active HTTP requests. Streams reset by the server are considered closed, even though backend + processing continues. This allows a client to cause the server to handle an unbounded number + of concurrent HTTP/2 requests on a single connection. + This is very similar to CVE-2019-9514 HTTP/2 Reset Flood + """ + server = self.get_server("deproxy") + flood_client = self.get_client("ctrl_frames_flood") + tempesta = self.get_tempesta() + + server.set_response( + "HTTP/1.1 200 OK\r\n" + + "Server: Debian\r\n" + + f"Date: {HttpMessage.date_time_string()}\r\n" + + "Content-Length: 2000\r\n\r\n" + + (2000 * "a") + ) + + self.start_all_services(client=False) + + flood_client.options = flood_client.options + [cmd_args] + flood_client.start() + self.wait_while_busy(flood_client) + flood_client.stop() + + self.assertEqual(0, flood_client.returncode) + tempesta.get_stats() + self.assertEqual(tempesta.stats.__dict__[stat_name], 100) diff --git a/tests/cve/test_cve_2019.py b/tests/cve/test_cve_2019.py deleted file mode 100644 index ddf9d9a04..000000000 --- a/tests/cve/test_cve_2019.py +++ /dev/null @@ -1,162 +0,0 @@ -__author__ = "Tempesta Technologies, Inc." -__copyright__ = "Copyright (C) 2026 Tempesta Technologies, Inc." -__license__ = "GPL2" - -from pathlib import Path - -from framework.helpers import remote, tf_cfg -from framework.test_suite import tester - - -class TestSlowRead(tester.TempestaTest): - clients = [ - { - "id": f"deproxy-{i}", - "type": "deproxy_h2", - "addr": "${tempesta_ip}", - "port": "443", - "ssl": True, - "ssl_hostname": "tempesta-tech.com", - } - for i in range(20) - ] - - backends = [ - { - "id": "nginx", - "type": "nginx", - "status_uri": "http://${server_ip}:8000/nginx_status", - "config": """ -pid ${pid}; -worker_processes auto; - -events { - worker_connections 1024; - use epoll; -} - -http { - keepalive_timeout ${server_keepalive_timeout}; - keepalive_requests 10; - sendfile on; - tcp_nopush on; - tcp_nodelay on; - - open_file_cache max=1000; - open_file_cache_valid 30s; - open_file_cache_min_uses 2; - open_file_cache_errors off; - - # [ debug | info | notice | warn | error | crit | alert | emerg ] - # Fully disable log errors. - error_log /dev/null emerg; - - # Disable access log altogether. - access_log off; - - server { - listen ${server_ip}:8000; - - location / { - root ${server_resources}; - } - location /nginx_status { - stub_status on; - } - } -} -""", - } - ] - - tempesta = { - "config": """ - cache 0; - keepalive_timeout 10; - listen 443 proto=h2; - - tls_match_any_server_name; - - srv_group default { - server ${server_ip}:8000; - } - - vhost tempesta-tech.com { - tls_certificate ${tempesta_workdir}/tempesta.crt; - tls_certificate_key ${tempesta_workdir}/tempesta.key; - proxy_pass default; - } - - """ - } - - response_file_name = "large.txt" - response_file_path = str(Path(tf_cfg.cfg.get("Server", "resources")) / response_file_name) - - @classmethod - def setUpClass(cls): - super().setUpClass() - remote.server.run_cmd( - f"fallocate -l {1024**2 * int(tf_cfg.cfg.get("General", "long_body_size"))} {cls.response_file_path}" - ) - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - remote.server.remove_file(cls.response_file_path) - - def test_cve_2019_9511(self): - """ - CVE-2019-9511 - “Data Dribble” - Some HTTP/2 implementations are vulnerable to window size manipulation - and stream prioritization manipulation, potentially leading to a denial of service. - The attacker requests a large amount of data from a specified resource over multiple streams. - They manipulate window size and stream priority to force the server to queue the data in 1-byte chunks. - Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both. - """ - self.start_all_services() - - request = self.get_clients()[0].create_request( - method="GET", - headers=[], - authority="tempesta-tech.com", - uri=f"/{self.response_file_name}", - ) - - for client in self.get_clients(): - client.update_initial_settings(initial_window_size=1) - client.send_bytes(client.h2_connection.data_to_send()) - self.assertTrue(client.wait_for_ack_settings()) - client.make_requests([request] * 100) - - for client in self.get_clients(): - client.wait_for_connection_close(strict=True) - - def test_cve_2019_9517(self): - """ - CVE-2019-9517 - “Internal Data Buffering” - Some HTTP/2 implementations are vulnerable to unconstrained internal data buffering, - potentially leading to a denial of service. The attacker opens the HTTP/2 window - so the peer can send without constraint; however, they leave the TCP window closed - so the peer cannot actually write (many of) the bytes on the wire. - The attacker sends a stream of requests for a large response object. - Depending on how the servers queue the responses, this can consume excess memory, CPU, or both. - """ - self.start_all_services() - - request = self.get_clients()[0].create_request( - method="GET", - headers=[], - authority="tempesta-tech.com", - uri=f"/{self.response_file_name}", - ) - - for client in self.get_clients(): - client.update_initial_settings() - client.send_bytes(client.h2_connection.data_to_send()) - self.assertTrue(client.wait_for_ack_settings()) - client.set_size_of_receiving_buffer(new_buffer_size=1) - client.make_requests([request] * 100) - - for client in self.get_clients(): - client.wait_for_connection_close(timeout=20, strict=True) diff --git a/tests/stress/test_stress.py b/tests/stress/test_stress.py index ae61290cb..37fcb482d 100644 --- a/tests/stress/test_stress.py +++ b/tests/stress/test_stress.py @@ -6,11 +6,9 @@ import time from pathlib import Path -from framework.deproxy.deproxy_message import HttpMessage from framework.helpers import dmesg, networker, remote, tf_cfg from framework.services import tempesta from framework.test_suite import marks, tester -from tests.frang.frang_test_case import FrangTestCase __author__ = "Tempesta Technologies, Inc." __copyright__ = "Copyright (C) 2022-2026 Tempesta Technologies, Inc." @@ -796,207 +794,3 @@ async def test_h2_post_request(self): async def test_h2_put_request(self): await self._test_h2load(method="PUT") - - -class TestContinuationFlood(tester.TempestaTest): - """ - Test stability against CONTINUATION frame flood. - """ - - clients = [ - { - "id": "gflood", - "type": "external", - "binary": "gflood", - "ssl": True, - "cmd_args": "-address ${tempesta_ip}:443 -host tempesta-tech.com -threads 4 -connections 10000 -streams 100 -headers_cnt 7", - }, - ] - - backends = [ - { - "id": "nginx", - "type": "nginx", - "port": "8000", - "status_uri": "http://${server_ip}:8000/nginx_status", - "config": NGINX_CONFIG, - } - ] - - tempesta = { - "config": """ - listen 443 proto=h2; - - server ${server_ip}:8000; - - tls_certificate ${tempesta_workdir}/tempesta.crt; - tls_certificate_key ${tempesta_workdir}/tempesta.key; - tls_match_any_server_name; - cache 0; - """ - } - - @dmesg.limited_rate_on_tempesta_node - async def test(self): - client = self.get_client("gflood") - - await self.start_all_services(client=True) - await self.wait_while_busy(client) - client.stop() - self.assertEqual(0, client.returncode) - - -class TestRequestsUnderCtrlFrameFlood(FrangTestCase): - """ - Test ability to handle requests from the client - under control frames frame flood. - Also check that there is no kernel BUGS and WARNINGs - under flood. - """ - - backends = [ - { - "id": "deproxy", - "type": "deproxy", - "port": "8000", - "response": "static", - "response_content": ( - "HTTP/1.1 200 OK\r\n" - + f"Date: {HttpMessage.date_time_string()}\r\n" - + "Server: debian\r\n" - + "Content-Length: 0\r\n\r\n" - ), - } - ] - - tempesta = { - "config": """ - listen 443 proto=h2; - - server ${server_ip}:8000; - - tls_certificate ${tempesta_workdir}/tempesta.crt; - tls_certificate_key ${tempesta_workdir}/tempesta.key; - tls_match_any_server_name; - cache 0; - """ - } - - clients = [ - { - "id": "ctrl_frames_flood", - "type": "external", - "binary": "ctrl_frames_flood", - "ssl": True, - "cmd_args": "", - }, - ] - - async def _test(self, cmd_args): - await self.start_all_services(client=False) - flood_client = self.get_client("ctrl_frames_flood") - flood_client.options = [cmd_args % tf_cfg.cfg.get("Tempesta", "ip")] - flood_client.start() - await self.wait_while_busy(flood_client) - flood_client.stop() - - def _check_ping_frame_exceeded(self): - tempesta = self.get_tempesta() - tempesta.get_stats() - self.assertEqual(tempesta.stats.cl_ping_frame_exceeded, 100) - - def _check_prio_frame_exceeded(self): - tempesta = self.get_tempesta() - tempesta.get_stats() - self.assertEqual(tempesta.stats.cl_priority_frame_exceeded, 100) - - def _check_settings_frame_exceeded(self): - tempesta = self.get_tempesta() - stats = tempesta.get_stats() - self.assertEqual(tempesta.stats.cl_settings_frame_exceeded, 100) - - def _check_wnd_update_frame_exceeded(self): - tempesta = self.get_tempesta() - stats = tempesta.get_stats() - self.assertEqual(tempesta.stats.cl_wnd_update_frame_exceeded, 100) - - def _check_rst_frame_exceeded(self): - tempesta = self.get_tempesta() - stats = tempesta.get_stats() - self.assertEqual(tempesta.stats.cl_rst_frame_exceeded, 100) - - @marks.Parameterize.expand( - [ - marks.Param( - name="PingFlood", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type ping_frame -frame_count 100000", - check_func=_check_ping_frame_exceeded, - ), - marks.Param( - name="SettingsFlood", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type settings_frame -frame_count 100000", - check_func=_check_settings_frame_exceeded, - ), - marks.Param( - name="WndUpdateFlood", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type window_update -frame_count 100000", - check_func=_check_wnd_update_frame_exceeded, - ), - marks.Param( - name="RstFloodByWndUpdate", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type rapid_reset -rapid_reset_type window_update -frame_count 100000", - check_func=_check_rst_frame_exceeded, - ), - marks.Param( - name="RstFloodByPriority", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type rapid_reset -rapid_reset_type priority -frame_count 100000", - check_func=_check_rst_frame_exceeded, - ), - marks.Param( - name="RstFloodByRst", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type rapid_reset -rapid_reset_type rst -frame_count 100000", - check_func=_check_rst_frame_exceeded, - ), - marks.Param( - name="RstFloodByRstBatch", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type rapid_reset -rapid_reset_type batch -frame_count 100000", - check_func=_check_rst_frame_exceeded, - ), - marks.Param( - name="RstByHeadersMaxConcurrentStreamsExceeded", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type rapid_reset -rapid_reset_type headers_by_max_streams_exceeded -frame_count 100000", - check_func=_check_rst_frame_exceeded, - ), - marks.Param( - name="RstByHeadersInvalidDependency", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type rapid_reset -rapid_reset_type headers_by_invalid_dependency -frame_count 100000", - check_func=_check_rst_frame_exceeded, - ), - marks.Param( - name="RstByIncorrectFrameType", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type rapid_reset -rapid_reset_type incorrect_frame_type -frame_count 100000", - check_func=_check_rst_frame_exceeded, - ), - marks.Param( - name="RstByIncorrectHeader", - cmd_args=f"-address %s:443 -threads 4 -connections 100 -ctrl_frame_type rapid_reset -rapid_reset_type incorrect_header -frame_count 100000", - check_func=_check_rst_frame_exceeded, - ), - ] - ) - @dmesg.unlimited_rate_on_tempesta_node - async def test(self, name, cmd_args, check_func): - server = self.get_server("deproxy") - response_body = 2000 * "a" - server.set_response( - "HTTP/1.1 200 OK\r\n" - + "Server: Debian\r\n" - + f"Date: {HttpMessage.date_time_string()}\r\n" - + f"Content-Length: {len(response_body)}\r\n\r\n" - + response_body - ) - await self._test(cmd_args) - check_func(self) - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/tests_disabled.json b/tests/tests_disabled.json index 804b2becf..b3d70b842 100644 --- a/tests/tests_disabled.json +++ b/tests/tests_disabled.json @@ -189,28 +189,24 @@ "name": "tests.clickhouse.test_clickhouse_logs.TestClickHouseLogsDelay", "reason": "The response time varies each time, and it is not possible to predict it exactly." }, - { - "name": "tests.stress.test_stress.TestContinuationFlood", - "reason": "Disabled by test issue #817" - }, { "name": "tests.stress.test_stress.TestStressNoCacheMTU80", - "reason": "Disabled by test issue #817" + "reason": "Disabled by test issue #" }, { "name": "tests.http2_general.test_h2_frame.TestPostponedFrames", "reason": "Disabled by test issue #862" }, - { + { "name": "tests.server_connections.test_close_dead_connections.TestFinishH2StreamsByClient.test_drop_server_connection_for_goaway", "reason": "Disabled by issue #2626" }, { - "name": "tests.cve.test_cve_2019.TestSlowRead.test_cve_2019_9511", + "name": "tests.cve.test_cve.TestSlowRead.test_cve_2019_9511", "reason": "Disabled by issue #2627" }, { - "name": "tests.cve.test_cve_2019.TestSlowRead.test_cve_2019_9517", + "name": "tests.cve.test_cve.TestSlowRead.test_cve_2019_9517", "reason": "Disabled by issue #1715" } ] diff --git a/tests/tests_disabled_dbgkernel.json b/tests/tests_disabled_dbgkernel.json index ed81bc142..796dded22 100644 --- a/tests/tests_disabled_dbgkernel.json +++ b/tests/tests_disabled_dbgkernel.json @@ -22,7 +22,7 @@ "reason" : "the tests did not work on the first run" }, { - "name": "tests.stress.test_stress.TestContinuationFlood", + "name": "tests.cve.test_cve.TestHttp2FrameFlood.test_cve_2024_2758", "reason" : "the tests did not work on the first run" }, { diff --git a/tests/tests_disabled_remote.json b/tests/tests_disabled_remote.json index 2430ed5b1..00698e0d1 100644 --- a/tests/tests_disabled_remote.json +++ b/tests/tests_disabled_remote.json @@ -104,22 +104,18 @@ { "name": "tests.frang.test_concurrent_connections.TestConcurrentConnectionsNonTempesta", "reason": "Is not intended to run on remote setup. Local only." - }, - { - "name": "tests.stress.test_stress.TestContinuationFlood", - "reason": "Disabled by test issue #817" }, { "name": "tests.stress.test_stress.H2LoadStressMTU80", - "reason": "Disabled by test issue #817" + "reason": "Disabled by test issue #816" }, { "name": "tests.stress.test_stress.TlsWrkStressMTU80", - "reason": "Disabled by test issue #817" + "reason": "Disabled by test issue #816" }, { "name": "tests.stress.test_stress.WrkStressMTU80", - "reason": "Disabled by test issue #817" + "reason": "Disabled by test issue #816" } ] } From 66e571c40a1cb6c1be16ca17e686026677c04b0b Mon Sep 17 00:00:00 2001 From: Roman Belozerov Date: Wed, 11 Mar 2026 14:07:27 +0400 Subject: [PATCH 5/7] Move header leak tests to CVE directory --- tests/cve/test_cve.py | 140 ++++++++++++++++++++++++- tests/stress/test_header_leak.py | 157 ---------------------------- tests/tests_disabled_dbgkernel.json | 2 +- 3 files changed, 140 insertions(+), 159 deletions(-) diff --git a/tests/cve/test_cve.py b/tests/cve/test_cve.py index 08efa61f8..bf8f01cad 100644 --- a/tests/cve/test_cve.py +++ b/tests/cve/test_cve.py @@ -2,10 +2,14 @@ __copyright__ = "Copyright (C) 2026 Tempesta Technologies, Inc." __license__ = "GPL2" +import random +import string from pathlib import Path +from hyperframe.frame import HeadersFrame + from framework.deproxy.deproxy_message import HttpMessage -from framework.helpers import dmesg, remote, tf_cfg +from framework.helpers import dmesg, memworker, remote, tf_cfg from framework.test_suite import marks, tester @@ -353,3 +357,137 @@ def test_cve(self, name, cmd_args, stat_name): self.assertEqual(0, flood_client.returncode) tempesta.get_stats() self.assertEqual(tempesta.stats.__dict__[stat_name], 100) + + +class TestH2Headers(tester.TempestaTest): + + clients = [ + { + "id": "deproxy", + "type": "deproxy_h2", + "addr": "${tempesta_ip}", + "port": "443", + "ssl": True, + "ssl_hostname": "tempesta-tech.com", + }, + ] + + backends = [ + { + "id": "nginx", + "type": "nginx", + "status_uri": "http://${server_ip}:8000/nginx_status", + "config": """ +pid ${pid}; +worker_processes auto; +events { + worker_connections 1024; + use epoll; +} +http { + keepalive_timeout ${server_keepalive_timeout}; + keepalive_requests 10; + tcp_nopush on; + tcp_nodelay on; + error_log /dev/null emerg; + access_log off; + server { + listen ${server_ip}:8000; + location / { + return 200 'foo'; + } + location /nginx_status { + stub_status on; + } + } +} +""", + } + ] + + tempesta = { + "config": """ + cache 0; + + keepalive_timeout 1000; + + listen 443 proto=h2; + + tls_match_any_server_name; + max_concurrent_streams 10000; + + srv_group default { + server ${server_ip}:8000; + } + frang_limits { + http_strict_host_checking false; + http_header_cnt 1000; + } + + ctrl_frame_rate_multiplier 65536; + + vhost tempesta-tech.com { + tls_certificate ${tempesta_workdir}/tempesta.crt; + tls_certificate_key ${tempesta_workdir}/tempesta.key; + proxy_pass default; + } + """ + } + + @staticmethod + def randomword(length): + letters = string.ascii_lowercase + return "".join(random.choice(letters) for i in range(length)) + + @dmesg.limited_rate_on_tempesta_node + def test_2019_9516(self): + """ + CVE-2019-9516 “0-Length Headers Leak” + Some HTTP/2 implementations are vulnerable to a header leak, potentially leading to a denial of service. + The attacker sends a stream of headers with a 0-length header name and 0-length header value, + optionally Huffman encoded into 1-byte or greater headers. Some implementations allocate memory + for these headers and keep the allocation alive until the session dies. This can consume excess memory. + + 1. For 0-length header name, tempesta returns 400 and GOAWAY and + closes the connection, so no further effect happens after. + 2. If we send a request followed by RST_STREAM, temepesta will close + the connection when the response arrives from the backend, + so no further effect too. + """ + self.start_all_services() + client = self.get_client("deproxy") + + request = client.create_request( + uri="/", + method="GET", + headers=[(self.randomword(100), self.randomword(100)) for _ in range(50)], + ) + + # create http2 connection and stream 1. The stream and connection are open. + # It is necessary for check of a memory consumption + client.make_request(client.create_request(method="GET", headers=[]), end_stream=False) + + with memworker.check_memory_consumptions(self): + for stream_id in range(3, 20000, 2): + client.stream_id = stream_id + client.make_request(request, end_stream=False) + + # send trailer headers with invalid `x-forwarded-for` header + # it is necessary for calling the RST_STREAM + client.send_bytes( + HeadersFrame( + stream_id=stream_id, + data=client.h2_connection.encoder.encode( + [("x-forwarded-for", "1.1.1.1.1.1")] + ), + flags=["END_STREAM", "END_HEADERS"], + ).serialize(), + expect_response=True, + ) + + self.assertTrue(client.wait_for_response(120)) + + # close first stream and http2 connection and finish test + client.stream_id = 1 + client.make_request("data", end_stream=True) + self.assertTrue(client.wait_for_response()) diff --git a/tests/stress/test_header_leak.py b/tests/stress/test_header_leak.py index 8fbdeb5e6..e69de29bb 100644 --- a/tests/stress/test_header_leak.py +++ b/tests/stress/test_header_leak.py @@ -1,157 +0,0 @@ -__author__ = "Tempesta Technologies, Inc." -__copyright__ = "Copyright (C) 2024 Tempesta Technologies, Inc." -__license__ = "GPL2" - -import random -import re -import string - -import psutil -from hyperframe.frame import HeadersFrame - -from framework.helpers import dmesg, remote -from framework.test_suite import tester - - -def randomword(length): - letters = string.ascii_lowercase - return "".join(random.choice(letters) for i in range(length)) - - -def get_memory_lines(*names): - """Get values from /proc/meminfo""" - [stdout, stderr] = remote.tempesta.run_cmd("cat /proc/meminfo") - lines = [] - for name in names: - line = re.search("%s:[ ]+([0-9]+)" % name, str(stdout)) - if line: - lines.append(int(line.group(1))) - else: - raise Exception("Can not get %s from /proc/meminfo" % name) - return lines - - -class TestH2HeaderLeak(tester.TempestaTest): - """ - 1. For 0-length header name, tempesta returns 400 and GOAWAY and - closes the connection, so no further effect happens after. - 2. If we send a request followed by RST_STREAM, temepesta will close - the connection when the response arrives from the backend, - so no further effect too. - """ - - clients = [ - { - "id": "deproxy", - "type": "deproxy_h2", - "addr": "${tempesta_ip}", - "port": "443", - "ssl": True, - "ssl_hostname": "tempesta-tech.com", - }, - ] - - backends = [ - { - "id": "nginx", - "type": "nginx", - "status_uri": "http://${server_ip}:8000/nginx_status", - "config": """ -pid ${pid}; -worker_processes auto; -events { - worker_connections 1024; - use epoll; -} -http { - keepalive_timeout ${server_keepalive_timeout}; - keepalive_requests 10; - tcp_nopush on; - tcp_nodelay on; - error_log /dev/null emerg; - access_log off; - server { - listen ${server_ip}:8000; - location / { - return 200 'foo'; - } - location /nginx_status { - stub_status on; - } - } -} -""", - } - ] - - tempesta = { - "config": """ - cache 0; - - keepalive_timeout 1000; - - listen 443 proto=h2; - - tls_match_any_server_name; - max_concurrent_streams 10000; - - srv_group default { - server ${server_ip}:8000; - } - frang_limits { - http_strict_host_checking false; - http_header_cnt 1000; - } - - ctrl_frame_rate_multiplier 65536; - - vhost tempesta-tech.com { - tls_certificate ${tempesta_workdir}/tempesta.crt; - tls_certificate_key ${tempesta_workdir}/tempesta.key; - proxy_pass default; - } - """ - } - - @dmesg.limited_rate_on_tempesta_node - async def test(self): - await self.start_all_services() - client = self.get_client("deproxy") - - request = client.create_request( - uri="/", method="GET", headers=[(randomword(100), randomword(100)) for _ in range(50)] - ) - - # create http2 connection and stream 1. The stream and connection are open. - # It is necessary for check of a memory consumption - client.make_request(client.create_request(method="GET", headers=[]), end_stream=False) - - # save a memory consumption and make a lot of requests with different headers - (mem1,) = get_memory_lines("MemAvailable") - mem1 = mem1 + psutil.Process().memory_info().rss // 1024 # python memory - for stream_id in range(3, 20000, 2): - client.stream_id = stream_id - client.make_request(request, end_stream=False) - - # send trailer headers with invalid `x-forwarded-for` header - # it is necessary for calling the RST_STREAM - client.send_bytes( - HeadersFrame( - stream_id=stream_id, - data=client.h2_connection.encoder.encode([("x-forwarded-for", "1.1.1.1.1.1")]), - flags=["END_STREAM", "END_HEADERS"], - ).serialize(), - expect_response=True, - ) - - self.assertTrue(await client.wait_for_response(120)) - # check a memory consumption (http2 connection is still open) - (mem2,) = get_memory_lines("MemAvailable") - mem2 = mem2 + psutil.Process().memory_info().rss // 1024 - memdiff = abs(mem2 - mem1) / mem1 - self.assertLess(memdiff, 0.05) - - # close first stream and http2 connection and finish test - client.stream_id = 1 - client.make_request("data", end_stream=True) - self.assertTrue(await client.wait_for_response()) diff --git a/tests/tests_disabled_dbgkernel.json b/tests/tests_disabled_dbgkernel.json index 796dded22..f1e16c030 100644 --- a/tests/tests_disabled_dbgkernel.json +++ b/tests/tests_disabled_dbgkernel.json @@ -18,7 +18,7 @@ "reason" : "the tests did not work on the first run" }, { - "name": "tests.stress.test_header_leak.TestH2HeaderLeak", + "name": "tests.cve.test_cve.TestH2HeaderLeak", "reason" : "the tests did not work on the first run" }, { From e95cdd88ca16c2f1db2bd609be191b63a8a5664d Mon Sep 17 00:00:00 2001 From: Roman Belozerov Date: Thu, 12 Mar 2026 14:33:55 +0400 Subject: [PATCH 6/7] =?UTF-8?q?add=20simple=20tests=20for=20CVE-2019-9513?= =?UTF-8?q?=20=E2=80=9CResource=20Loop=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- framework/test_suite/tester.py | 12 +++---- tests/cve/test_cve.py | 63 +++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/framework/test_suite/tester.py b/framework/test_suite/tester.py index 55acde763..c619921a9 100644 --- a/framework/test_suite/tester.py +++ b/framework/test_suite/tester.py @@ -14,21 +14,21 @@ import run_config from framework.deproxy import deproxy_client, deproxy_manager from framework.deproxy.deproxy_auto_parser import DeproxyAutoParser -from framework.deproxy.deproxy_server import deproxy_srv_factory +from framework.deproxy.deproxy_server import StaticDeproxyServer, deproxy_srv_factory from framework.helpers import clickhouse, dmesg, error, remote, tf_cfg, util from framework.helpers.memworker import MemoryChecker from framework.helpers.networker import NetWorker from framework.helpers.tf_cfg import test_logger from framework.helpers.util import fill_template -from framework.services import base_server, curl_client, external_client +from framework.services import curl_client, external_client from framework.services import tempesta as tfw from framework.services import wrk_client -from framework.services.docker_server import docker_srv_factory -from framework.services.nginx_server import nginx_srv_factory +from framework.services.docker_server import DockerServer, docker_srv_factory +from framework.services.nginx_server import Nginx, nginx_srv_factory from framework.services.stateful import Stateful __author__ = "Tempesta Technologies, Inc." -__copyright__ = "Copyright (C) 2018-2025 Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2018-2026 Tempesta Technologies, Inc." __license__ = "GPL2" @@ -282,7 +282,7 @@ def __create_servers(self): # Copy description to keep it clean between several tests. self.__create_backend(server.copy()) - def get_server(self, sid: str | int) -> base_server.BaseServer: + def get_server(self, sid: str | int) -> StaticDeproxyServer | DockerServer | Nginx: """Return client with specified id""" server = self.__servers.get(sid, None) if server is None: diff --git a/tests/cve/test_cve.py b/tests/cve/test_cve.py index bf8f01cad..11cbe0b36 100644 --- a/tests/cve/test_cve.py +++ b/tests/cve/test_cve.py @@ -6,7 +6,7 @@ import string from pathlib import Path -from hyperframe.frame import HeadersFrame +from hyperframe.frame import HeadersFrame, PriorityFrame from framework.deproxy.deproxy_message import HttpMessage from framework.helpers import dmesg, memworker, remote, tf_cfg @@ -218,8 +218,69 @@ class TestHttp2FrameFlood(tester.TempestaTest): "-threads 4 -connections 10000 -streams 100 -headers_cnt 7" ), }, + { + "id": "deproxy", + "type": "deproxy_h2", + "addr": "${tempesta_ip}", + "port": "443", + "ssl": True, + }, ] + def random_depends_on(self, stream_id: int) -> int: + depends_on = random.randint(1, 199) + if depends_on % 2 == 0: + depends_on += 1 + if depends_on == stream_id: + return self.random_depends_on(stream_id) + return depends_on + + @staticmethod + def random_stream_id(max_id: int) -> int: + stream_id = random.randint(1, max_id) + if stream_id % 2 == 0: + stream_id = stream_id - 1 if stream_id == max_id else stream_id + 1 + return stream_id + + def test_cve_2019_9513(self): + """ + CVE-2019-9513 “Resource Loop” + Some HTTP/2 implementations are vulnerable to resource loops, potentially leading to a denial of service. + The attacker creates multiple request streams and continually shuffles the priority of the streams in a way + that causes substantial churn to the priority tree. This can consume excess CPU. + + Tempesta FW blocks a lot of priority frames. + """ + server = self.get_server("deproxy") + client = self.get_client("deproxy") + + server.set_response("") + + self.start_all_services(client=False) + + request = client.create_request(uri="/", method="GET", headers=[]) + + client.start() + client.make_request(request) + for stream_id in range(3, 200, 2): + client.make_request( + request, + priority_weight=random.randint(1, 255), + priority_depends_on=self.random_depends_on(stream_id), + priority_exclusive=bool(random.randint(0, 1)), + ) + + for _ in range(1000): + client.send_bytes( + PriorityFrame( + stream_id=self.random_stream_id(199), + depends_on=self.random_depends_on(stream_id), + stream_weight=random.randint(1, 255), + exclusive=bool(random.randint(0, 1)), + ).serialize() + ) + client.wait_for_connection_close(strict=True) + @dmesg.limited_rate_on_tempesta_node def test_cve_2024_2758(self): """ From 36e9d6fb4d0cf1d79a895aeb7107b05eae56594d Mon Sep 17 00:00:00 2001 From: Roman Belozerov Date: Thu, 19 Mar 2026 16:03:39 +0400 Subject: [PATCH 7/7] - add async to new tests; - update tests_retry, some frang tests are unstable yet; --- tests/cve/test_cve.py | 42 +++++++++++++++++++++--------------------- tests/tests_retry | 2 ++ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/tests/cve/test_cve.py b/tests/cve/test_cve.py index 11cbe0b36..aa05589f3 100644 --- a/tests/cve/test_cve.py +++ b/tests/cve/test_cve.py @@ -110,7 +110,7 @@ def tearDownClass(cls): super().tearDownClass() remote.server.remove_file(cls.response_file_path) - def test_cve_2019_9511(self): + async def test_cve_2019_9511(self): """ CVE-2019-9511 - “Data Dribble” Some HTTP/2 implementations are vulnerable to window size manipulation @@ -119,7 +119,7 @@ def test_cve_2019_9511(self): They manipulate window size and stream priority to force the server to queue the data in 1-byte chunks. Depending on how efficiently this data is queued, this can consume excess CPU, memory, or both. """ - self.start_all_services() + await self.start_all_services() request = self.get_clients()[0].create_request( method="GET", @@ -131,13 +131,13 @@ def test_cve_2019_9511(self): for client in self.get_clients(): client.update_initial_settings(initial_window_size=1) client.send_bytes(client.h2_connection.data_to_send()) - self.assertTrue(client.wait_for_ack_settings()) + self.assertTrue(await client.wait_for_ack_settings()) client.make_requests([request] * 100) for client in self.get_clients(): - client.wait_for_connection_close(strict=True) + await client.wait_for_connection_close(strict=True) - def test_cve_2019_9517(self): + async def test_cve_2019_9517(self): """ CVE-2019-9517 - “Internal Data Buffering” Some HTTP/2 implementations are vulnerable to unconstrained internal data buffering, @@ -147,7 +147,7 @@ def test_cve_2019_9517(self): The attacker sends a stream of requests for a large response object. Depending on how the servers queue the responses, this can consume excess memory, CPU, or both. """ - self.start_all_services() + await self.start_all_services() request = self.get_clients()[0].create_request( method="GET", @@ -159,12 +159,12 @@ def test_cve_2019_9517(self): for client in self.get_clients(): client.update_initial_settings() client.send_bytes(client.h2_connection.data_to_send()) - self.assertTrue(client.wait_for_ack_settings()) + self.assertTrue(await client.wait_for_ack_settings()) client.set_size_of_receiving_buffer(new_buffer_size=1) client.make_requests([request] * 100) for client in self.get_clients(): - client.wait_for_connection_close(timeout=20, strict=True) + await client.wait_for_connection_close(timeout=20, strict=True) class TestHttp2FrameFlood(tester.TempestaTest): @@ -242,7 +242,7 @@ def random_stream_id(max_id: int) -> int: stream_id = stream_id - 1 if stream_id == max_id else stream_id + 1 return stream_id - def test_cve_2019_9513(self): + async def test_cve_2019_9513(self): """ CVE-2019-9513 “Resource Loop” Some HTTP/2 implementations are vulnerable to resource loops, potentially leading to a denial of service. @@ -256,7 +256,7 @@ def test_cve_2019_9513(self): server.set_response("") - self.start_all_services(client=False) + await self.start_all_services(client=False) request = client.create_request(uri="/", method="GET", headers=[]) @@ -279,10 +279,10 @@ def test_cve_2019_9513(self): exclusive=bool(random.randint(0, 1)), ).serialize() ) - client.wait_for_connection_close(strict=True) + await client.wait_for_connection_close(strict=True) @dmesg.limited_rate_on_tempesta_node - def test_cve_2024_2758(self): + async def test_cve_2024_2758(self): """ CVE-2024-2758 "Continuation flood" Many HTTP/2 implementations do not properly limit or sanitize the amount of CONTINUATION frames sent within @@ -290,12 +290,12 @@ def test_cve_2024_2758(self): frames that will not be appended to the header list in memory but will still be processed and decoded by the server or will be appended to the header list, causing an out of memory (OOM) crash. """ - self.start_all_services(client=False) + await self.start_all_services(client=False) client = self.get_client("gflood") client.start() - self.wait_while_busy(client) + await self.wait_while_busy(client) client.stop() self.assertEqual(0, client.returncode) @@ -360,7 +360,7 @@ def test_cve_2024_2758(self): ] ) @dmesg.unlimited_rate_on_tempesta_node - def test_cve(self, name, cmd_args, stat_name): + async def test_cve(self, name, cmd_args, stat_name): """ CVE-2019-9512 “Ping Flood” Some HTTP/2 implementations are vulnerable to ping floods, potentially leading to a denial of service. @@ -408,11 +408,11 @@ def test_cve(self, name, cmd_args, stat_name): + (2000 * "a") ) - self.start_all_services(client=False) + await self.start_all_services(client=False) flood_client.options = flood_client.options + [cmd_args] flood_client.start() - self.wait_while_busy(flood_client) + await self.wait_while_busy(flood_client) flood_client.stop() self.assertEqual(0, flood_client.returncode) @@ -501,7 +501,7 @@ def randomword(length): return "".join(random.choice(letters) for i in range(length)) @dmesg.limited_rate_on_tempesta_node - def test_2019_9516(self): + async def test_2019_9516(self): """ CVE-2019-9516 “0-Length Headers Leak” Some HTTP/2 implementations are vulnerable to a header leak, potentially leading to a denial of service. @@ -515,7 +515,7 @@ def test_2019_9516(self): the connection when the response arrives from the backend, so no further effect too. """ - self.start_all_services() + await self.start_all_services() client = self.get_client("deproxy") request = client.create_request( @@ -546,9 +546,9 @@ def test_2019_9516(self): expect_response=True, ) - self.assertTrue(client.wait_for_response(120)) + self.assertTrue(await client.wait_for_response(120)) # close first stream and http2 connection and finish test client.stream_id = 1 client.make_request("data", end_stream=True) - self.assertTrue(client.wait_for_response()) + self.assertTrue(await client.wait_for_response()) diff --git a/tests/tests_retry b/tests/tests_retry index e69de29bb..ba8a5061c 100644 --- a/tests/tests_retry +++ b/tests/tests_retry @@ -0,0 +1,2 @@ +tests.frang.test_request_rate_burst +tests.frang.test_http_resp_code_block