Version in base suite: 9.2.5+ds-0+deb12u2 Base version: trafficserver_9.2.5+ds-0+deb12u2 Target version: trafficserver_9.2.5+ds-0+deb12u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/t/trafficserver/trafficserver_9.2.5+ds-0+deb12u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/t/trafficserver/trafficserver_9.2.5+ds-0+deb12u3.dsc changelog | 7 patches/CVE-2024-53868.patch | 496 ++++++++++++++++++++++++++++++++++++ patches/CVE-2025-31698.patch | 584 +++++++++++++++++++++++++++++++++++++++++++ patches/CVE-2025-49763.patch | 423 +++++++++++++++++++++++++++++++ patches/series | 3 5 files changed, 1513 insertions(+) diff -Nru trafficserver-9.2.5+ds/debian/changelog trafficserver-9.2.5+ds/debian/changelog --- trafficserver-9.2.5+ds/debian/changelog 2025-03-13 19:17:50.000000000 +0000 +++ trafficserver-9.2.5+ds/debian/changelog 2025-06-23 16:08:02.000000000 +0000 @@ -1,3 +1,10 @@ +trafficserver (9.2.5+ds-0+deb12u3) bookworm-security; urgency=medium + + * CVE-2024-53868 (Closes: #1101996) + * CVE-2025-31698, CVE-2025-49763 (Closes: #1108044) + + -- Moritz Mühlenhoff Mon, 23 Jun 2025 21:06:25 +0200 + trafficserver (9.2.5+ds-0+deb12u2) bookworm-security; urgency=medium * Update to 9.2.9 via 0023-update-to-929.patch (but not via a new diff -Nru trafficserver-9.2.5+ds/debian/patches/CVE-2024-53868.patch trafficserver-9.2.5+ds/debian/patches/CVE-2024-53868.patch --- trafficserver-9.2.5+ds/debian/patches/CVE-2024-53868.patch 1970-01-01 00:00:00.000000000 +0000 +++ trafficserver-9.2.5+ds/debian/patches/CVE-2024-53868.patch 2025-06-23 16:05:02.000000000 +0000 @@ -0,0 +1,496 @@ +From 3d2f29c88f9b073cb0fd3b9c7f85430e2170acbb Mon Sep 17 00:00:00 2001 +From: Masakazu Kitajo +Date: Tue, 1 Apr 2025 12:15:16 -0600 +Subject: [PATCH] Require the use of CRLF in chunked message body (#12150) + +--- trafficserver-9.2.5+ds.orig/doc/admin-guide/files/records.config.en.rst ++++ trafficserver-9.2.5+ds/doc/admin-guide/files/records.config.en.rst +@@ -987,6 +987,15 @@ mptcp + for details about chunked trailers. By default, this option is disabled + and therefore |TS| will not drop chunked trailers. + ++.. ts:cv:: CONFIG proxy.config.http.strict_chunk_parsing INT 1 ++ :reloadable: ++ :overridable: ++ ++ Specifies whether |TS| strictly checks errors in chunked message body. ++ If enabled (``1``), |TS| returns 400 Bad Request if chunked message body is ++ not compliant with RFC 9112. If disabled (``0``), |TS| allows using LF as ++ a line terminator. ++ + .. ts:cv:: CONFIG proxy.config.http.send_http11_requests INT 1 + :reloadable: + :overridable: +--- trafficserver-9.2.5+ds.orig/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst ++++ trafficserver-9.2.5+ds/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst +@@ -111,6 +111,7 @@ TSOverridableConfigKey Value + :c:enumerator:`TS_CONFIG_HTTP_CACHE_WHEN_TO_REVALIDATE` :ts:cv:`proxy.config.http.cache.when_to_revalidate` + :c:enumerator:`TS_CONFIG_HTTP_CHUNKING_ENABLED` :ts:cv:`proxy.config.http.chunking_enabled` + :c:enumerator:`TS_CONFIG_HTTP_CHUNKING_SIZE` :ts:cv:`proxy.config.http.chunking.size` ++:c:enumerator:`TS_CONFIG_HTTP_STRICT_CHUNK_PARSING` :ts:cv:`proxy.config.http.strict_chunk_parsing` + :c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DEAD_SERVER` :ts:cv:`proxy.config.http.connect_attempts_max_retries_dead_server` + :c:enumerator:`TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS` :ts:cv:`proxy.config.http.drop_chunked_trailers` + :c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES` :ts:cv:`proxy.config.http.connect_attempts_max_retries` +--- trafficserver-9.2.5+ds.orig/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst ++++ trafficserver-9.2.5+ds/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst +@@ -91,6 +91,7 @@ Enumeration Members + .. c:enumerator:: TS_CONFIG_NET_SOCK_PACKET_TOS_OUT + .. c:enumerator:: TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE + .. c:enumerator:: TS_CONFIG_HTTP_CHUNKING_SIZE ++.. c:enumerator:: TS_CONFIG_HTTP_STRICT_CHUNK_PARSING + .. c:enumerator:: TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS + .. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED + .. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK +--- trafficserver-9.2.5+ds.orig/include/ts/apidefs.h.in ++++ trafficserver-9.2.5+ds/include/ts/apidefs.h.in +@@ -875,6 +875,7 @@ typedef enum { + TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS, + TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS, + TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS, ++ TS_CONFIG_HTTP_STRICT_CHUNK_PARSING, + TS_CONFIG_LAST_ENTRY + } TSOverridableConfigKey; + +--- trafficserver-9.2.5+ds.orig/mgmt/RecordsConfig.cc ++++ trafficserver-9.2.5+ds/mgmt/RecordsConfig.cc +@@ -363,6 +363,8 @@ static const RecordElement RecordsConfig + , + {RECT_CONFIG, "proxy.config.http.drop_chunked_trailers", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, "[0-1]", RECA_NULL} + , ++ {RECT_CONFIG, "proxy.config.http.strict_chunk_parsing", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_NULL, "[0-1]", RECA_NULL} ++ , + {RECT_CONFIG, "proxy.config.http.flow_control.enabled", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} + , + {RECT_CONFIG, "proxy.config.http.flow_control.high_water", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} +--- trafficserver-9.2.5+ds.orig/plugins/lua/ts_lua_http_config.c ++++ trafficserver-9.2.5+ds/plugins/lua/ts_lua_http_config.c +@@ -149,6 +149,7 @@ typedef enum { + TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE = TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE, + TS_LUA_CONFIG_ENABLE_PARENT_TIMEOUT_MARKDOWNS = TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS, + TS_LUA_CONFIG_DISABLE_PARENT_MARKDOWNS = TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS, ++ TS_LUA_CONFIG_HTTP_STRICT_CHUNK_PARSING = TS_CONFIG_HTTP_STRICT_CHUNK_PARSING, + TS_LUA_CONFIG_LAST_ENTRY = TS_CONFIG_LAST_ENTRY, + } TSLuaOverridableConfigKey; + +@@ -290,6 +291,7 @@ ts_lua_var_item ts_lua_http_config_vars[ + TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE), + TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_ENABLE_PARENT_TIMEOUT_MARKDOWNS), + TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_DISABLE_PARENT_MARKDOWNS), ++ TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_STRICT_CHUNK_PARSING), + TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_LAST_ENTRY), + }; + +--- trafficserver-9.2.5+ds.orig/proxy/http/HttpConfig.cc ++++ trafficserver-9.2.5+ds/proxy/http/HttpConfig.cc +@@ -1190,6 +1190,7 @@ HttpConfig::startup() + HttpEstablishStaticConfigByte(c.oride.chunking_enabled, "proxy.config.http.chunking_enabled"); + HttpEstablishStaticConfigLongLong(c.oride.http_chunking_size, "proxy.config.http.chunking.size"); + HttpEstablishStaticConfigByte(c.oride.http_drop_chunked_trailers, "proxy.config.http.drop_chunked_trailers"); ++ HttpEstablishStaticConfigByte(c.oride.http_strict_chunk_parsing, "proxy.config.http.strict_chunk_parsing"); + HttpEstablishStaticConfigByte(c.oride.flow_control_enabled, "proxy.config.http.flow_control.enabled"); + HttpEstablishStaticConfigLongLong(c.oride.flow_high_water_mark, "proxy.config.http.flow_control.high_water"); + HttpEstablishStaticConfigLongLong(c.oride.flow_low_water_mark, "proxy.config.http.flow_control.low_water"); +@@ -1496,6 +1497,7 @@ HttpConfig::reconfigure() + params->oride.keep_alive_enabled_out = INT_TO_BOOL(m_master.oride.keep_alive_enabled_out); + params->oride.chunking_enabled = INT_TO_BOOL(m_master.oride.chunking_enabled); + params->oride.http_drop_chunked_trailers = m_master.oride.http_drop_chunked_trailers; ++ params->oride.http_strict_chunk_parsing = m_master.oride.http_strict_chunk_parsing; + params->oride.auth_server_session_private = INT_TO_BOOL(m_master.oride.auth_server_session_private); + + params->oride.http_chunking_size = m_master.oride.http_chunking_size; +--- trafficserver-9.2.5+ds.orig/proxy/http/HttpConfig.h ++++ trafficserver-9.2.5+ds/proxy/http/HttpConfig.h +@@ -703,6 +703,7 @@ struct OverridableHttpConfigParams { + + MgmtInt http_chunking_size = 4096; // Maximum chunk size for chunked output. + MgmtByte http_drop_chunked_trailers = 0; ///< Whether to drop chunked trailers. ++ MgmtByte http_strict_chunk_parsing = 1; ///< Whether to parse chunked body strictly. + MgmtInt flow_high_water_mark = 0; ///< Flow control high water mark. + MgmtInt flow_low_water_mark = 0; ///< Flow control low water mark. + +--- trafficserver-9.2.5+ds.orig/proxy/http/HttpSM.cc ++++ trafficserver-9.2.5+ds/proxy/http/HttpSM.cc +@@ -978,7 +978,8 @@ HttpSM::wait_for_full_body() + p = tunnel.add_producer(ua_entry->vc, post_bytes, buf_start, &HttpSM::tunnel_handler_post_ua, HT_BUFFER_READ, "ua post buffer"); + if (chunked) { + bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; +- tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers); ++ bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; ++ tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers, parse_chunk_strictly); + } + ua_entry->in_tunnel = true; + ua_txn->set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf->transaction_no_activity_timeout_in)); +@@ -6197,10 +6198,11 @@ HttpSM::do_setup_post_tunnel(HttpVC_t to + // In either case, the server will support chunked (HTTP/1.1) + if (chunked) { + bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; ++ bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; + if (ua_txn->is_chunked_encoding_supported()) { +- tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers); ++ tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers, parse_chunk_strictly); + } else { +- tunnel.set_producer_chunking_action(p, 0, TCA_CHUNK_CONTENT, drop_chunked_trailers); ++ tunnel.set_producer_chunking_action(p, 0, TCA_CHUNK_CONTENT, drop_chunked_trailers, parse_chunk_strictly); + tunnel.set_producer_chunking_size(p, 0); + } + } +@@ -6609,7 +6611,9 @@ HttpSM::setup_cache_read_transfer() + // w/o providing a Content-Length header + if (t_state.client_info.receive_chunked_response) { + bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; +- tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers); ++ bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; ++ tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers, ++ parse_chunk_strictly); + tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size); + } + ua_entry->in_tunnel = true; +@@ -6927,8 +6931,10 @@ HttpSM::setup_server_transfer_to_transfo + transform_info.entry->in_tunnel = true; + + if (t_state.current.server->transfer_encoding == HttpTransact::CHUNKED_ENCODING) { +- client_response_hdr_bytes = 0; // fixed by YTS Team, yamsat +- tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_DECHUNK_CONTENT, HttpTunnel::DROP_CHUNKED_TRAILERS); ++ client_response_hdr_bytes = 0; // fixed by YTS Team, yamsat ++ bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; ++ tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_DECHUNK_CONTENT, HttpTunnel::DROP_CHUNKED_TRAILERS, ++ parse_chunk_strictly); + } + + return p; +@@ -6968,7 +6974,9 @@ HttpSM::setup_transfer_from_transform() + + if (t_state.client_info.receive_chunked_response) { + bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; +- tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers); ++ bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; ++ tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers, ++ parse_chunk_strictly); + tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size); + } + +@@ -7025,7 +7033,8 @@ HttpSM::setup_server_transfer_to_cache_o + tunnel.add_producer(server_entry->vc, nbytes, buf_start, &HttpSM::tunnel_handler_server, HT_HTTP_SERVER, "http server"); + + bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; +- tunnel.set_producer_chunking_action(p, 0, action, drop_chunked_trailers); ++ bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; ++ tunnel.set_producer_chunking_action(p, 0, action, drop_chunked_trailers, parse_chunk_strictly); + tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size); + + setup_cache_write_transfer(&cache_sm, server_entry->vc, &t_state.cache_info.object_store, 0, "cache write"); +@@ -7114,7 +7123,8 @@ HttpSM::setup_server_transfer() + } + */ + bool const drop_chunked_trailers = t_state.http_config_param->oride.http_drop_chunked_trailers == 1; +- tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action, drop_chunked_trailers); ++ bool const parse_chunk_strictly = t_state.http_config_param->oride.http_strict_chunk_parsing == 1; ++ tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action, drop_chunked_trailers, parse_chunk_strictly); + tunnel.set_producer_chunking_size(p, t_state.txn_conf->http_chunking_size); + return p; + } +--- trafficserver-9.2.5+ds.orig/proxy/http/HttpTunnel.cc ++++ trafficserver-9.2.5+ds/proxy/http/HttpTunnel.cc +@@ -51,27 +51,28 @@ static int const CHUNK_IOBUFFER_SIZE_IND + ChunkedHandler::ChunkedHandler() : max_chunk_size(DEFAULT_MAX_CHUNK_SIZE) {} + + void +-ChunkedHandler::init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers) ++ChunkedHandler::init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers, bool parse_chunk_strictly) + { + if (p->do_chunking) { +- init_by_action(buffer_in, ACTION_DOCHUNK, drop_chunked_trailers); ++ init_by_action(buffer_in, ACTION_DOCHUNK, drop_chunked_trailers, parse_chunk_strictly); + } else if (p->do_dechunking) { +- init_by_action(buffer_in, ACTION_DECHUNK, drop_chunked_trailers); ++ init_by_action(buffer_in, ACTION_DECHUNK, drop_chunked_trailers, parse_chunk_strictly); + } else { +- init_by_action(buffer_in, ACTION_PASSTHRU, drop_chunked_trailers); ++ init_by_action(buffer_in, ACTION_PASSTHRU, drop_chunked_trailers, parse_chunk_strictly); + } + return; + } + + void +-ChunkedHandler::init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers) ++ChunkedHandler::init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers, bool parse_chunk_strictly) + { +- running_sum = 0; +- num_digits = 0; +- cur_chunk_size = 0; +- cur_chunk_bytes_left = 0; +- truncation = false; +- this->action = action; ++ running_sum = 0; ++ num_digits = 0; ++ cur_chunk_size = 0; ++ cur_chunk_bytes_left = 0; ++ truncation = false; ++ this->action = action; ++ this->strict_chunk_parsing = parse_chunk_strictly; + + switch (action) { + case ACTION_DOCHUNK: +@@ -139,7 +140,6 @@ ChunkedHandler::read_size() + { + int64_t bytes_consumed = 0; + bool done = false; +- int cr = 0; + + while (chunked_reader->is_read_avail_more_than(0) && !done) { + const char *tmp = chunked_reader->start(); +@@ -178,36 +178,59 @@ ChunkedHandler::read_size() + done = true; + break; + } else { +- if (ParseRules::is_cr(*tmp)) { +- ++cr; ++ if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) { ++ ++num_cr; + } + state = CHUNK_READ_SIZE_CRLF; // now look for CRLF + } + } + } else if (state == CHUNK_READ_SIZE_CRLF) { // Scan for a linefeed + if (ParseRules::is_lf(*tmp)) { ++ if (!prev_is_cr) { ++ Debug("http_chunk", "Found an LF without a preceding CR (protocol violation)"); ++ if (strict_chunk_parsing) { ++ state = CHUNK_READ_ERROR; ++ done = true; ++ break; ++ } ++ } + Debug("http_chunk", "read chunk size of %d bytes", running_sum); + cur_chunk_bytes_left = (cur_chunk_size = running_sum); + state = (running_sum == 0) ? CHUNK_READ_TRAILER_BLANK : CHUNK_READ_CHUNK; + done = true; +- cr = 0; ++ num_cr = 0; + break; +- } else if (ParseRules::is_cr(*tmp)) { +- if (cr != 0) { ++ } else if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) { ++ if (num_cr != 0) { + state = CHUNK_READ_ERROR; + done = true; + break; + } +- ++cr; ++ ++num_cr; + } + } else if (state == CHUNK_READ_SIZE_START) { +- if (ParseRules::is_cr(*tmp)) { +- // Skip it +- } else if (ParseRules::is_lf(*tmp) && +- bytes_used <= 2) { // bytes_used should be 2 if it's CRLF, but permit a single LF as well ++ Debug("http_chunk", "CHUNK_READ_SIZE_START 0x%02x", *tmp); ++ if (ParseRules::is_lf(*tmp)) { ++ if (!prev_is_cr) { ++ Debug("http_chunk", "Found an LF without a preceding CR (protocol violation) before chunk size"); ++ if (strict_chunk_parsing) { ++ state = CHUNK_READ_ERROR; ++ done = true; ++ break; ++ } ++ } + running_sum = 0; + num_digits = 0; ++ num_cr = 0; + state = CHUNK_READ_SIZE; ++ } else if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) { ++ if (num_cr != 0) { ++ Debug("http_chunk", "Found multiple CRs before chunk size"); ++ state = CHUNK_READ_ERROR; ++ done = true; ++ break; ++ } ++ ++num_cr; + } else { // Unexpected character + state = CHUNK_READ_ERROR; + done = true; +@@ -651,9 +674,10 @@ HttpTunnel::deallocate_buffers() + + void + HttpTunnel::set_producer_chunking_action(HttpTunnelProducer *p, int64_t skip_bytes, TunnelChunkingAction_t action, +- bool drop_chunked_trailers) ++ bool drop_chunked_trailers, bool parse_chunk_strictly) + { + this->http_drop_chunked_trailers = drop_chunked_trailers; ++ this->http_strict_chunk_parsing = parse_chunk_strictly; + p->chunked_handler.skip_bytes = skip_bytes; + p->chunking_action = action; + +@@ -878,7 +902,7 @@ HttpTunnel::producer_run(HttpTunnelProdu + // For all the chunking cases, we must only copy bytes as we process them. + body_bytes_to_copy = 0; + +- p->chunked_handler.init(p->buffer_start, p, this->http_drop_chunked_trailers); ++ p->chunked_handler.init(p->buffer_start, p, this->http_drop_chunked_trailers, this->http_strict_chunk_parsing); + + // Copy the header into the chunked/dechunked buffers. + if (p->do_chunking) { +--- trafficserver-9.2.5+ds.orig/proxy/http/HttpTunnel.h ++++ trafficserver-9.2.5+ds/proxy/http/HttpTunnel.h +@@ -112,6 +112,8 @@ struct ChunkedHandler { + */ + bool drop_chunked_trailers = false; + ++ bool strict_chunk_parsing = true; ++ + bool truncation = false; + + /** The number of bytes to skip from the reader because they are not body bytes. +@@ -130,6 +132,8 @@ struct ChunkedHandler { + // Chunked header size parsing info. + int running_sum = 0; + int num_digits = 0; ++ int num_cr = 0; ++ bool prev_is_cr = false; + + /// @name Output data. + //@{ +@@ -144,8 +148,8 @@ struct ChunkedHandler { + //@} + ChunkedHandler(); + +- void init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers); +- void init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers); ++ void init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers, bool strict_parsing); ++ void init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers, bool strict_parsing); + void clear(); + + /// Set the max chunk @a size. +@@ -392,6 +396,7 @@ public: + + /// A named variable for the @a drop_chunked_trailers parameter to @a set_producer_chunking_action. + static constexpr bool DROP_CHUNKED_TRAILERS = true; ++ static constexpr bool PARSE_CHUNK_STRICTLY = true; + + /** Designate chunking behavior to the producer. + * +@@ -402,9 +407,10 @@ public: + * @param[in] drop_chunked_trailers If @c true, chunked trailers are filtered + * out. Logically speaking, this is only applicable when proxying chunked + * content, thus only when @a action is @c TCA_PASSTHRU_CHUNKED_CONTENT. ++ * @param[in] parse_chunk_strictly If @c true, no parse error will be allowed + */ + void set_producer_chunking_action(HttpTunnelProducer *p, int64_t skip_bytes, TunnelChunkingAction_t action, +- bool drop_chunked_trailers); ++ bool drop_chunked_trailers, bool parse_chunk_strictly); + /// Set the maximum (preferred) chunk @a size of chunked output for @a producer. + void set_producer_chunking_size(HttpTunnelProducer *producer, int64_t size); + +@@ -483,6 +489,9 @@ private: + /// Corresponds to proxy.config.http.drop_chunked_trailers having a value of 1. + bool http_drop_chunked_trailers = false; + ++ /// Corresponds to proxy.config.http.strict_chunk_parsing having a value of 1. ++ bool http_strict_chunk_parsing = false; ++ + /** The number of body bytes processed in this last execution of the tunnel. + * + * This accounting is used to determine how many bytes to copy into the body +--- trafficserver-9.2.5+ds.orig/src/shared/overridable_txn_vars.cc ++++ trafficserver-9.2.5+ds/src/shared/overridable_txn_vars.cc +@@ -31,6 +31,7 @@ const std::unordered_mapinit_by_action(resp_reader, ChunkedHandler::ACTION_DECHUNK, HttpTunnel::DROP_CHUNKED_TRAILERS); ++ ch->init_by_action(resp_reader, ChunkedHandler::ACTION_DECHUNK, HttpTunnel::DROP_CHUNKED_TRAILERS, ++ HttpTunnel::PARSE_CHUNK_STRICTLY); + ch->dechunked_reader = ch->dechunked_buffer->alloc_reader(); + ch->state = ChunkedHandler::CHUNK_READ_SIZE; + resp_reader->dealloc(); +--- trafficserver-9.2.5+ds.orig/src/traffic_server/InkAPI.cc ++++ trafficserver-9.2.5+ds/src/traffic_server/InkAPI.cc +@@ -8928,6 +8928,9 @@ _conf_to_memberp(TSOverridableConfigKey + case TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS: + ret = _memberp_to_generic(&overridableHttpConfig->http_drop_chunked_trailers, conv); + break; ++ case TS_CONFIG_HTTP_STRICT_CHUNK_PARSING: ++ ret = _memberp_to_generic(&overridableHttpConfig->http_strict_chunk_parsing, conv); ++ break; + case TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED: + ret = _memberp_to_generic(&overridableHttpConfig->flow_control_enabled, conv); + break; +--- trafficserver-9.2.5+ds.orig/src/traffic_server/InkAPITest.cc ++++ trafficserver-9.2.5+ds/src/traffic_server/InkAPITest.cc +@@ -8774,7 +8774,8 @@ std::array httpSMAllocator; + +--- trafficserver-9.2.5+ds.orig/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml ++++ trafficserver-9.2.5+ds/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml +@@ -98,6 +98,27 @@ sessions: + server-response: + status: 200 + ++- transactions: ++ - client-request: ++ method: "POST" ++ version: "1.1" ++ url: /malformed/chunk/header3 ++ headers: ++ fields: ++ - [ Host, example.com ] ++ - [ Transfer-Encoding, chunked ] ++ - [ uuid, 5 ] ++ content: ++ transfer: plain ++ encoding: uri ++ # Chunk header must end with a sequence of CRLF. ++ data: 7;x%0Aabcwxyz%0D%0A0%0D%0A%0D%0A ++ ++ # The connection will be dropped and this response will not go out. ++ server-response: ++ status: 200 ++ ++ + # + # Now repeat the above two malformed chunk header tests, but on the server + # side. +@@ -193,3 +214,26 @@ sessions: + encoding: uri + # BWS cannot have CR + data: 3%0D%0D%0Adef%0D%0A0%0D%0A%0D%0A ++ ++- transactions: ++ - client-request: ++ method: "GET" ++ version: "1.1" ++ url: /response/malformed/chunk/size2 ++ headers: ++ fields: ++ - [ Host, example.com ] ++ - [ uuid, 105 ] ++ ++ # The connection will be dropped and this response will not go out. ++ server-response: ++ status: 200 ++ reason: OK ++ headers: ++ fields: ++ - [ Transfer-Encoding, chunked ] ++ content: ++ transfer: plain ++ encoding: uri ++ # Chunk header must end with a sequence of CRLF. ++ data: 3;x%0Adef%0D%0A0%0D%0A%0D%0A diff -Nru trafficserver-9.2.5+ds/debian/patches/CVE-2025-31698.patch trafficserver-9.2.5+ds/debian/patches/CVE-2025-31698.patch --- trafficserver-9.2.5+ds/debian/patches/CVE-2025-31698.patch 1970-01-01 00:00:00.000000000 +0000 +++ trafficserver-9.2.5+ds/debian/patches/CVE-2025-31698.patch 2025-06-23 16:05:39.000000000 +0000 @@ -0,0 +1,584 @@ +From ce942e0acacd5cc9f38bd07565a1dfc5ffed0e33 Mon Sep 17 00:00:00 2001 +From: Masakazu Kitajo +Date: Mon, 16 Jun 2025 15:30:56 -0600 +Subject: [PATCH] Add a setting to choose the data source of IP address for ACL + (#12294) + +--- trafficserver-9.2.5+ds.orig/doc/admin-guide/configuration/proxy-protocol.en.rst ++++ trafficserver-9.2.5+ds/doc/admin-guide/configuration/proxy-protocol.en.rst +@@ -47,6 +47,10 @@ configured with :ts:cv:`proxy.config.htt + If the allowlist is configured, requests will only be accepted from these + IP addresses and must be prefaced with the PROXY v1/v2 header. + ++By default, |TS| uses client's IP address that is from the peer when it applies ACL. If you configure a port to ++enable PROXY protocol and want to apply ACL against the IP address delivered by PROXY protocol, you need to have ``PROXY`` in ++:ts:cv:`proxy.config.acl.subjects`. ++ + 1. HTTP Forwarded Header + + The client IP address in the PROXY protocol header is passed to the origin server via an HTTP `Forwarded: +--- trafficserver-9.2.5+ds.orig/doc/admin-guide/files/records.config.en.rst ++++ trafficserver-9.2.5+ds/doc/admin-guide/files/records.config.en.rst +@@ -2102,6 +2102,20 @@ Security + policies is set in the corresponding :file:`sni.yaml` file entry and the :file:`sni.yaml` entry does not override + this value via a :ref:`host_sni_policy` attribute. + ++.. ts:cv:: CONFIG proxy.config.acl.subjects STRING PEER ++ ++ Specifies the list of data sources for getting client's IP address for ACL. ++ The value is a comma separated string, and the first available data source ++ will be used. If you configure a port to enable PROXY protocol, you probably ++ need to adjust this setting to have ``PROXY`` in the list. ++ ++ ============= ====================================================================== ++ Value Description ++ ============= ====================================================================== ++ ``PEER`` Use the IP address of the peer ++ ``PROXY`` Use the IP address from PROXY protocol ++ ============= ====================================================================== ++ + + Cache Control + ============= +--- trafficserver-9.2.5+ds.orig/iocore/net/P_SNIActionPerformer.h ++++ trafficserver-9.2.5+ds/iocore/net/P_SNIActionPerformer.h +@@ -35,6 +35,7 @@ + #include "SSLTypes.h" + + #include "tscore/ink_inet.h" ++#include "../../proxy/IPAllow.h" + + #include + +@@ -442,15 +443,25 @@ public: + return SSL_TLSEXT_ERR_OK; + } + +- auto ssl_vc = dynamic_cast(snis); +- auto ip = ssl_vc->get_remote_endpoint(); ++ auto ssl_vc = dynamic_cast(snis); ++ const sockaddr *ip = nullptr; ++ for (int i = 0; i < IpAllow::Subject::MAX_SUBJECTS; ++i) { ++ if (IpAllow::Subject::PEER == IpAllow::subjects[i]) { ++ ip = ssl_vc->get_remote_addr(); ++ break; ++ } else if (IpAllow::Subject::PROXY == IpAllow::subjects[i] && ++ ssl_vc->get_proxy_protocol_version() != ProxyProtocolVersion::UNDEFINED) { ++ ip = ssl_vc->get_proxy_protocol_src_addr(); ++ break; ++ } ++ } + + // check the allowed ips + if (ip_map.contains(ip)) { + return SSL_TLSEXT_ERR_OK; + } else { + char buff[256]; +- ats_ip_ntop(&ip.sa, buff, sizeof(buff)); ++ ats_ip_ntop(ip, buff, sizeof(buff)); + Debug("ssl_sni", "%s is not allowed. Denying connection", buff); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } +--- trafficserver-9.2.5+ds.orig/iocore/net/libinknet_stub.cc ++++ trafficserver-9.2.5+ds/iocore/net/libinknet_stub.cc +@@ -192,3 +192,5 @@ PreWarmManager::reconfigure() + } + + PreWarmManager prewarmManager; ++ ++uint8_t IpAllow::subjects[Subject::MAX_SUBJECTS]; +--- trafficserver-9.2.5+ds.orig/mgmt/RecordsConfig.cc ++++ trafficserver-9.2.5+ds/mgmt/RecordsConfig.cc +@@ -149,6 +149,14 @@ static const RecordElement RecordsConfig + + //############################################################################## + //# ++ //# ACL ++ //# ++ //############################################################################## ++ {RECT_CONFIG, "proxy.config.acl.subjects", RECD_STRING, "PEER", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL} ++ , ++ ++ //############################################################################## ++ //# + //# Support for disabling check for Accept-* / Content-* header mismatch + //# + //############################################################################## +--- trafficserver-9.2.5+ds.orig/proxy/IPAllow.cc ++++ trafficserver-9.2.5+ds/proxy/IPAllow.cc +@@ -25,6 +25,8 @@ + */ + + #include ++#include ++ + #include "IPAllow.h" + #include "tscore/BufferWriter.h" + #include "tscore/ts_file.h" +@@ -114,6 +116,7 @@ const IpAllow::ACL IpAllow::DENY_ALL_ACL + + size_t IpAllow::configid = 0; + bool IpAllow::accept_check_p = true; // initializing global flag for fast deny ++uint8_t IpAllow::subjects[Subject::MAX_SUBJECTS]; + + static ConfigUpdateHandler *ipAllowUpdate; + +@@ -202,7 +205,35 @@ IpAllow::match(sockaddr const *ip, match + // End API functions + // + +-IpAllow::IpAllow(const char *config_var) : config_file(ats_scoped_str(RecConfigReadConfigPath(config_var)).get()) {} ++IpAllow::IpAllow(const char *config_var) : config_file(ats_scoped_str(RecConfigReadConfigPath(config_var)).get()) ++{ ++ RecString subjects_char; ++ REC_ReadConfigStringAlloc(subjects_char, "proxy.config.acl.subjects"); ++ std::string_view subjects_sv{subjects_char}; ++ int i = 0; ++ std::string_view::size_type s, e; ++ ++ for (s = 0, e = 0; s < subjects_sv.size() && e != subjects_sv.npos; s = e + 1) { ++ e = subjects_sv.find(",", s); ++ std::string_view subject_sv = subjects_sv.substr(s, e); ++ if (i >= MAX_SUBJECTS) { ++ Error("Too many ACL subjects were provided"); ++ } ++ if (subject_sv == "PEER") { ++ subjects[i] = Subject::PEER; ++ ++i; ++ } else if (subject_sv == "PROXY") { ++ subjects[i] = Subject::PROXY; ++ ++i; ++ } else { ++ Debug("ip-allow", "Unknown subject %.*s was ignored", static_cast(subject_sv.length()), subject_sv.data()); ++ } ++ } ++ if (i < Subject::MAX_SUBJECTS) { ++ subjects[i] = Subject::MAX_SUBJECTS; ++ } ++ ats_free(subjects_char); ++} + + void + IpAllow::PrintMap(const IpMap *map) const +--- trafficserver-9.2.5+ds.orig/proxy/IPAllow.h ++++ trafficserver-9.2.5+ds/proxy/IPAllow.h +@@ -102,6 +102,8 @@ public: + + static constexpr const char *MODULE_NAME = "IPAllow"; + ++ enum Subject { PEER, PROXY, MAX_SUBJECTS }; ++ + /** An access control record and support data. + * The primary point of this is to hold the backing configuration in memory while the ACL + * is in use. +@@ -182,6 +184,8 @@ public: + + const ts::file::path &get_config_file() const; + ++ static uint8_t subjects[Subject::MAX_SUBJECTS]; ++ + private: + static size_t configid; ///< Configuration ID for update management. + static const Record ALLOW_ALL_RECORD; ///< Static record that allows all access. +--- trafficserver-9.2.5+ds.orig/proxy/http/HttpSessionAccept.cc ++++ trafficserver-9.2.5+ds/proxy/http/HttpSessionAccept.cc +@@ -33,6 +33,16 @@ HttpSessionAccept::accept(NetVConnection + IpAllow::ACL acl; + ip_port_text_buffer ipb; + ++ for (int i = 0; i < IpAllow::Subject::MAX_SUBJECTS; ++i) { ++ if (IpAllow::Subject::PEER == IpAllow::subjects[i]) { ++ client_ip = netvc->get_remote_addr(); ++ break; ++ } else if (IpAllow::Subject::PROXY == IpAllow::subjects[i] && ++ netvc->get_proxy_protocol_version() != ProxyProtocolVersion::UNDEFINED) { ++ client_ip = netvc->get_proxy_protocol_src_addr(); ++ break; ++ } ++ } + acl = IpAllow::match(client_ip, IpAllow::SRC_ADDR); + if (!acl.isValid()) { // if there's no ACL, it's a hard deny. + Warning("client '%s' prohibited by ip-allow policy", ats_ip_ntop(client_ip, ipb, sizeof(ipb))); +--- trafficserver-9.2.5+ds.orig/proxy/http/remap/UrlRewrite.cc ++++ trafficserver-9.2.5+ds/proxy/http/remap/UrlRewrite.cc +@@ -402,6 +402,26 @@ UrlRewrite::PerformACLFiltering(HttpTran + bool client_enabled_flag = true; + + ink_release_assert(ats_is_ip(&s->client_info.src_addr)); ++ const IpEndpoint *src_addr = nullptr; ++ const IpEndpoint *local_addr = nullptr; ++ const ProxyProtocol &pp_info = s->state_machine->get_ua_txn()->get_netvc()->get_proxy_protocol_info(); ++ for (int i = 0; i < IpAllow::Subject::MAX_SUBJECTS; ++i) { ++ if (IpAllow::Subject::PEER == IpAllow::subjects[i]) { ++ src_addr = &s->client_info.src_addr; ++ local_addr = &s->client_info.dst_addr; ++ break; ++ } else if (IpAllow::Subject::PROXY == IpAllow::subjects[i] && pp_info.version != ProxyProtocolVersion::UNDEFINED) { ++ src_addr = &pp_info.src_addr; ++ local_addr = &pp_info.dst_addr; ++ break; ++ } ++ } ++ ++ if (src_addr == nullptr) { ++ // Use addresses from peer if none of the configured sources are avaialable ++ src_addr = &s->client_info.src_addr; ++ local_addr = &s->client_info.dst_addr; ++ } + + for (acl_filter_rule *rp = map->filter; rp && client_enabled_flag; rp = rp->next) { + bool match = true; +@@ -421,7 +441,7 @@ UrlRewrite::PerformACLFiltering(HttpTran + if (match && rp->src_ip_valid) { + match = false; + for (int j = 0; j < rp->src_ip_cnt && !match; j++) { +- bool in_range = rp->src_ip_array[j].contains(s->client_info.src_addr); ++ bool in_range = rp->src_ip_array[j].contains(*src_addr); + if (rp->src_ip_array[j].invert) { + if (!in_range) { + match = true; +@@ -438,16 +458,14 @@ UrlRewrite::PerformACLFiltering(HttpTran + Debug("url_rewrite", "match was true and we have specified a in_ip field"); + match = false; + for (int j = 0; j < rp->in_ip_cnt && !match; j++) { +- IpEndpoint incoming_addr; +- incoming_addr.assign(s->state_machine->ua_txn->get_netvc()->get_local_addr()); + if (is_debug_tag_set("url_rewrite")) { + char buf1[128], buf2[128], buf3[128]; +- ats_ip_ntop(incoming_addr, buf1, sizeof(buf1)); ++ ats_ip_ntop(local_addr, buf1, sizeof(buf1)); + rp->in_ip_array[j].start.toString(buf2, sizeof(buf2)); + rp->in_ip_array[j].end.toString(buf3, sizeof(buf3)); + Debug("url_rewrite", "Trying to match incoming address %s in range %s - %s.", buf1, buf2, buf3); + } +- bool in_range = rp->in_ip_array[j].contains(incoming_addr); ++ bool in_range = rp->in_ip_array[j].contains(*local_addr); + if (rp->in_ip_array[j].invert) { + if (!in_range) { + match = true; +--- trafficserver-9.2.5+ds.orig/proxy/http2/Http2SessionAccept.cc ++++ trafficserver-9.2.5+ds/proxy/http2/Http2SessionAccept.cc +@@ -36,8 +36,20 @@ Http2SessionAccept::~Http2SessionAccept( + bool + Http2SessionAccept::accept(NetVConnection *netvc, MIOBuffer *iobuf, IOBufferReader *reader) + { +- sockaddr const *client_ip = netvc->get_remote_addr(); +- IpAllow::ACL session_acl = IpAllow::match(client_ip, IpAllow::SRC_ADDR); ++ sockaddr const *client_ip = nullptr; ++ ++ for (int i = 0; i < IpAllow::Subject::MAX_SUBJECTS; ++i) { ++ if (IpAllow::Subject::PEER == IpAllow::subjects[i]) { ++ client_ip = netvc->get_remote_addr(); ++ break; ++ } else if (IpAllow::Subject::PROXY == IpAllow::subjects[i] && ++ netvc->get_proxy_protocol_version() != ProxyProtocolVersion::UNDEFINED) { ++ client_ip = netvc->get_proxy_protocol_src_addr(); ++ break; ++ } ++ } ++ ++ IpAllow::ACL session_acl = IpAllow::match(client_ip, IpAllow::SRC_ADDR); + if (!session_acl.isValid()) { + ip_port_text_buffer ipb; + Warning("HTTP/2 client '%s' prohibited by ip-allow policy", ats_ip_ntop(client_ip, ipb, sizeof(ipb))); +--- trafficserver-9.2.5+ds.orig/tests/gold_tests/autest-site/trafficserver.test.ext ++++ trafficserver-9.2.5+ds/tests/gold_tests/autest-site/trafficserver.test.ext +@@ -44,7 +44,7 @@ def MakeATSProcess( + enable_quic=False, + block_for_debug=False, + log_data=default_log_data, +- use_traffic_out=True): ++ use_traffic_out=True, enable_proxy_protocol=False): + ##################################### + # common locations + +@@ -328,6 +328,14 @@ def MakeATSProcess( + if enable_tls: + get_port(p, "ssl_port") + get_port(p, "ssl_portv6") ++ ++ if enable_proxy_protocol: ++ get_port(p, "proxy_protocol_port") ++ get_port(p, "proxy_protocol_portv6") ++ ++ if enable_tls: ++ get_port(p, "proxy_protocol_ssl_port") ++ get_port(p, "proxy_protocol_ssl_portv6") + else: + p.Variables.port = 8080 + p.Variables.portv6 = 8080 +@@ -381,6 +389,10 @@ def MakeATSProcess( + if enable_quic: + port_str += " {ssl_port}:quic {ssl_portv6}:quic:ipv6".format( + ssl_port=p.Variables.ssl_port, ssl_portv6=p.Variables.ssl_portv6) ++ if enable_proxy_protocol: ++ port_str += f" {p.Variables.proxy_protocol_port}:pp {p.Variables.proxy_protocol_portv6}:pp:ipv6" ++ if enable_tls: ++ port_str += f" {p.Variables.proxy_protocol_ssl_port}:pp:ssl {p.Variables.proxy_protocol_ssl_portv6}:pp:ssl:ipv6" + #p.Env['PROXY_CONFIG_HTTP_SERVER_PORTS'] = port_str + p.Disk.records_config.update({ + 'proxy.config.http.server_ports': port_str, +--- trafficserver-9.2.5+ds.orig/tests/gold_tests/remap/remap_acl.test.py ++++ trafficserver-9.2.5+ds/tests/gold_tests/remap/remap_acl.test.py +@@ -43,7 +43,7 @@ class Test_remap_acl: + + def __init__( + self, name: str, replay_file: str, ip_allow_content: str, deactivate_ip_allow: bool, +- acl_configuration: str, named_acls: List[Tuple[str, str]], expected_responses: List[int]): ++ acl_configuration: str, named_acls: List[Tuple[str, str]], expected_responses: List[int], proxy_protocol: bool): + """Initialize the test. + + :param name: The name of the test. +@@ -64,7 +64,7 @@ class Test_remap_acl: + tr = Test.AddTestRun(name) + self._configure_server(tr) + self._configure_traffic_server(tr) +- self._configure_client(tr) ++ self._configure_client(tr, proxy_protocol) + + def _configure_server(self, tr: 'TestRun') -> None: + """Configure the server. +@@ -83,16 +83,17 @@ class Test_remap_acl: + """ + + name = f"ts-{Test_remap_acl._ts_counter}" +- ts = tr.MakeATSProcess(name, enable_cache=False, enable_tls=True) ++ ts = tr.MakeATSProcess(name, enable_cache=False, enable_tls=True, enable_proxy_protocol=True) + Test_remap_acl._ts_counter += 1 + self._ts = ts + + ts.Disk.records_config.update( + { + 'proxy.config.diags.debug.enabled': 1, +- 'proxy.config.diags.debug.tags': 'http|url|remap|ip_allow', ++ 'proxy.config.diags.debug.tags': 'http|url|remap|ip-allow|proxyprotocol', + 'proxy.config.http.push_method_enabled': 1, + 'proxy.config.http.connect_ports': self._server.Variables.http_port, ++ 'proxy.config.acl.subjects': 'PROXY,PEER', + }) + + remap_config_lines = [] +@@ -111,14 +112,15 @@ class Test_remap_acl: + ts.Disk.remap_config.AddLines(remap_config_lines) + ts.Disk.ip_allow_yaml.AddLines(self._ip_allow_content.split("\n")) + +- def _configure_client(self, tr: 'TestRun') -> None: ++ def _configure_client(self, tr: 'TestRun', proxy_protocol: bool) -> None: + """Run the test. + + :param tr: The TestRun object to associate the client process with. + """ + + name = f"client-{Test_remap_acl._client_counter}" +- p = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[self._ts.Variables.port]) ++ port = self._ts.Variables.port if proxy_protocol == False else self._ts.Variables.proxy_protocol_port ++ p = tr.AddVerifierClientProcess(name, self._replay_file, http_ports=[port]) + Test_remap_acl._client_counter += 1 + p.StartBefore(self._server) + p.StartBefore(self._ts) +@@ -159,6 +161,25 @@ def replay_proxy_response(filename, repl + with open(replay_file, "w") as f: + f.write(dump(data)) + ++IP_ALLOW_CONTENT = f''' ++ip_allow: ++ - apply: in ++ ip_addrs: 0/0 ++ action: allow ++ methods: ++ - GET ++''' ++ ++test_ip_allow_optional_methods_pp = Test_remap_acl( ++ "Verify non-allowed methods are blocked (PP).", ++ replay_file='remap_acl_get_post_allowed_pp.replay.yaml', ++ ip_allow_content=IP_ALLOW_CONTENT, ++ deactivate_ip_allow=True, ++ acl_configuration='@action=allow @src_ip=1.2.3.4 @method=GET @method=POST', ++ named_acls=[], ++ expected_responses=[200, 200, 403, 403, 403], ++ proxy_protocol=True) ++ + """ + Test all acl combinations + # """ +@@ -178,6 +199,7 @@ for idx, test in enumerate(all_acl_combi + acl_configuration=test["inline"], + named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], + expected_responses=[test["GET response"], test["POST response"]], ++ proxy_protocol=False, + ) + + """ +@@ -202,7 +224,9 @@ for idx, test in enumerate(all_deactivat + deactivate_ip_allow=test["deactivate_ip_allow"], + acl_configuration=test["inline"], + named_acls=[("acl", test["named_acl"])] if test["named_acl"] != "" else [], +- expected_responses=[test["GET response"], test["POST response"]]) ++ expected_responses=[test["GET response"], test["POST response"]], ++ proxy_protocol=False, ++ ) + + """ + Test combination of named filters +@@ -223,4 +247,5 @@ for idx, test in enumerate(named_filter_ + acl_configuration="", + named_acls=[("acl_1", test["named_acl_1"]), ("acl_2", test["named_acl_2"])], + expected_responses=[test["GET response"], test["POST response"]], ++ proxy_protocol=False, + ) +--- /dev/null ++++ trafficserver-9.2.5+ds/tests/gold_tests/remap/remap_acl_get_post_allowed_pp.replay.yaml +@@ -0,0 +1,152 @@ ++# Licensed to the Apache Software Foundation (ASF) under one ++# or more contributor license agreements. See the NOTICE file ++# distributed with this work for additional information ++# regarding copyright ownership. The ASF licenses this file ++# to you under the Apache License, Version 2.0 (the ++# "License"); you may not use this file except in compliance ++# with the License. You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++ ++# This expects a remap.config that allows GET and POST, but denies all other ++# methods. ++ ++meta: ++ version: "1.0" ++ ++ blocks: ++ - standard_response: &standard_response ++ server-response: ++ status: 200 ++ reason: OK ++ headers: ++ fields: ++ - [ Content-Length, 20 ] ++ ++sessions: ++- protocol: ++ - name: http ++ version: 1 ++ - name: proxy-protocol ++ version: 2 ++ src-addr: "1.2.3.4:1111" ++ dst-addr: "5.6.7.8:2222" ++ transactions: ++ ++ - client-request: ++ method: "GET" ++ version: "1.1" ++ url: /test/ip_allow/test_get ++ headers: ++ fields: ++ - [ Content-Length, 0 ] ++ - [ uuid, get ] ++ - [ X-Request, get ] ++ ++ <<: *standard_response ++ ++ proxy-response: ++ status: 200 ++ ++ # POST also is in the allow list. ++ - client-request: ++ method: "POST" ++ version: "1.1" ++ url: /test/ip_allow/test_post ++ headers: ++ fields: ++ - [Content-Length, 10] ++ - [ uuid, post ] ++ - [ X-Request, post ] ++ ++ <<: *standard_response ++ ++ proxy-response: ++ status: 200 ++ ++ # PUT rejected ++ - client-request: ++ method: "PUT" ++ version: "1.1" ++ url: /test/ip_allow/test_put ++ headers: ++ fields: ++ - [ Host, example.com ] ++ - [ uuid, put ] ++ - [ X-Request, put ] ++ - [ Content-Length, 113 ] ++ content: ++ encoding: plain ++ data: "HTTP/1.1 200 OK\nServer: ATS/10.0.0\nAccept-Ranges: bytes\nContent-Length: 6\nCache-Control: public,max-age=2\n\nCACHED" ++ ++ # Not received. ++ <<: *standard_response ++ ++ # Verify that ATS rejected the PUSH. ++ proxy-response: ++ status: 403 ++ ++- protocol: ++ - name: http ++ version: 1 ++ - name: proxy-protocol ++ version: 2 ++ src-addr: "1.2.3.4:1111" ++ dst-addr: "5.6.7.8:2222" ++ delay: 2s ++ transactions: ++ ++ # DELETE rejected ++ - client-request: ++ method: "DELETE" ++ version: "1.1" ++ url: /test/ip_allow/test_delete ++ headers: ++ fields: ++ - [ Host, example.com ] ++ - [ uuid, delete ] ++ - [ X-Request, delete ] ++ - [ Content-Length, 0 ] ++ ++ <<: *standard_response ++ ++ # Verify that ATS rejects the DELETE. ++ proxy-response: ++ status: 403 ++ ++- protocol: ++ - name: http ++ version: 1 ++ - name: proxy-protocol ++ version: 2 ++ src-addr: "1.2.3.4:1111" ++ dst-addr: "5.6.7.8:2222" ++ delay: 4s ++ transactions: ++ ++ # PUSH rejected ++ - client-request: ++ method: "PUSH" ++ version: "1.1" ++ url: /test/ip_allow/test_push ++ headers: ++ fields: ++ - [ Host, example.com ] ++ - [ uuid, push ] ++ - [ X-Request, push ] ++ - [ Content-Length, 113 ] ++ content: ++ encoding: plain ++ data: "HTTP/1.1 200 OK\nServer: ATS/10.0.0\nAccept-Ranges: bytes\nContent-Length: 6\nCache-Control: public,max-age=2\n\nCACHED" ++ ++ <<: *standard_response ++ ++ # Verify that ATS rejected the PUSH. ++ proxy-response: ++ status: 403 diff -Nru trafficserver-9.2.5+ds/debian/patches/CVE-2025-49763.patch trafficserver-9.2.5+ds/debian/patches/CVE-2025-49763.patch --- trafficserver-9.2.5+ds/debian/patches/CVE-2025-49763.patch 1970-01-01 00:00:00.000000000 +0000 +++ trafficserver-9.2.5+ds/debian/patches/CVE-2025-49763.patch 2025-06-23 16:06:11.000000000 +0000 @@ -0,0 +1,423 @@ +From 2db8b8dc96e57fc292850f77b9783630cc9590b9 Mon Sep 17 00:00:00 2001 +From: Kit Chan +Date: Mon, 16 Jun 2025 14:30:43 -0700 +Subject: [PATCH] Add max inclusion support to esi plugin for 9.2.x (#12296) + +--- trafficserver-9.2.5+ds.orig/doc/admin-guide/plugins/esi.en.rst ++++ trafficserver-9.2.5+ds/doc/admin-guide/plugins/esi.en.rst +@@ -76,7 +76,7 @@ Enabling ESI + + esi.so + +-2. There are four optional arguments that can be passed to the above ``esi.so`` entry: ++2. There are optional arguments that can be passed to the above ``esi.so`` entry: + + - ``--private-response`` will add private cache control and expires headers to the processed ESI document. + - ``--packed-node-support`` will enable the support for using the packed node feature, which will improve the +@@ -86,6 +86,8 @@ Enabling ESI + - ``--first-byte-flush`` will enable the first byte flush feature, which will flush content to users as soon as the entire + ESI document is received and parsed without all ESI includes fetched. The flushing will stop at the ESI include markup + till that include is fetched. ++- ``--max-inclusion-depth `` controls the maximum depth of recursive ESI inclusion allowed (between 0 and 9). ++ Default is 3. + + 3. ``HTTP_COOKIE`` variable support is turned off by default. It can be turned on with ``-f `` or + ``-handler ``. For example: +--- trafficserver-9.2.5+ds.orig/plugins/esi/esi.cc ++++ trafficserver-9.2.5+ds/plugins/esi/esi.cc +@@ -56,6 +56,7 @@ struct OptionInfo { + bool private_response; + bool disable_gzip_output; + bool first_byte_flush; ++ unsigned max_inclusion_depth{3}; + }; + + static HandlerManager *gHandlerManager = nullptr; +@@ -74,6 +75,9 @@ static Utils::HeaderValueList gAllowlist + #define MIME_FIELD_XESI "X-Esi" + #define MIME_FIELD_XESI_LEN 5 + ++#define MIME_FIELD_XESIDEPTH "X-Esi-Depth" ++#define MIME_FIELD_XESIDEPTH_LEN 11 ++ + #define HTTP_VALUE_PRIVATE_EXPIRES "-1" + #define HTTP_VALUE_PRIVATE_CC "max-age=0, private" + +@@ -315,7 +319,9 @@ ContData::getClientState() + } + TSHandleMLocRelease(bufp, req_hdr_loc, url_loc); + } ++ + TSMLoc field_loc = TSMimeHdrFieldGet(req_bufp, req_hdr_loc, 0); ++ bool depth_field = false; + while (field_loc) { + TSMLoc next_field_loc; + const char *name; +@@ -323,38 +329,55 @@ ContData::getClientState() + + name = TSMimeHdrFieldNameGet(req_bufp, req_hdr_loc, field_loc, &name_len); + if (name) { +- int n_values; +- n_values = TSMimeHdrFieldValuesCount(req_bufp, req_hdr_loc, field_loc); +- if (n_values && (n_values != TS_ERROR)) { +- const char *value = nullptr; +- int value_len = 0; +- if (n_values == 1) { +- value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, 0, &value_len); +- +- if (nullptr != value && value_len) { +- if (Utils::areEqual(name, name_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING) && +- Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { +- gzip_output = true; +- } +- } ++ if (Utils::areEqual(name, name_len, MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN)) { ++ unsigned d = TSMimeHdrFieldValueUintGet(req_bufp, req_hdr_loc, field_loc, -1); ++ d = (d + 1) % 10; ++ char dstr[2]; ++ int const len = snprintf(dstr, sizeof(dstr), "%u", d); ++ ++ HttpHeader header; ++ if (len != 1) { ++ header = HttpHeader(MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN, "1", 1); + } else { +- for (int i = 0; i < n_values; ++i) { +- value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, i, &value_len); ++ header = HttpHeader(MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN, dstr, 1); ++ } ++ data_fetcher->useHeader(header); ++ esi_vars->populate(header); ++ depth_field = true; ++ ++ } else { ++ int n_values; ++ n_values = TSMimeHdrFieldValuesCount(req_bufp, req_hdr_loc, field_loc); ++ if (n_values && (n_values != TS_ERROR)) { ++ const char *value = nullptr; ++ int value_len = 0; ++ if (n_values == 1) { ++ value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, 0, &value_len); ++ + if (nullptr != value && value_len) { + if (Utils::areEqual(name, name_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING) && + Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { + gzip_output = true; + } + } ++ } else { ++ for (int i = 0; i < n_values; ++i) { ++ value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, i, &value_len); ++ if (nullptr != value && value_len) { ++ if (Utils::areEqual(name, name_len, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING) && ++ Utils::areEqual(value, value_len, TS_HTTP_VALUE_GZIP, TS_HTTP_LEN_GZIP)) { ++ gzip_output = true; ++ } ++ } ++ } ++ value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, -1, &value_len); + } + +- value = TSMimeHdrFieldValueStringGet(req_bufp, req_hdr_loc, field_loc, -1, &value_len); +- } +- +- if (value != nullptr) { +- HttpHeader header(name, name_len, value, value_len); +- data_fetcher->useHeader(header); +- esi_vars->populate(header); ++ if (value != nullptr) { ++ HttpHeader header(name, name_len, value, value_len); ++ data_fetcher->useHeader(header); ++ esi_vars->populate(header); ++ } + } + } + } +@@ -363,6 +386,12 @@ ContData::getClientState() + TSHandleMLocRelease(req_bufp, req_hdr_loc, field_loc); + field_loc = next_field_loc; + } ++ ++ if (depth_field == false) { ++ HttpHeader header(MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN, "1", 1); ++ data_fetcher->useHeader(header); ++ esi_vars->populate(header); ++ } + } + + if (gzip_output) { +@@ -1252,7 +1281,7 @@ maskOsCacheHeaders(TSHttpTxn txnp) + } + + static bool +-isTxnTransformable(TSHttpTxn txnp, bool is_cache_txn, bool *intercept_header, bool *head_only) ++isTxnTransformable(TSHttpTxn txnp, bool is_cache_txn, const OptionInfo *pOptionInfo, bool *intercept_header, bool *head_only) + { + // We are only interested in transforming "200 OK" responses with a + // Content-Type: text/ header and with X-Esi header +@@ -1267,6 +1296,21 @@ isTxnTransformable(TSHttpTxn txnp, bool + return false; + } + ++ TSMLoc loc; ++ unsigned d; ++ ++ d = 0; ++ loc = TSMimeHdrFieldFind(bufp, hdr_loc, MIME_FIELD_XESIDEPTH, MIME_FIELD_XESIDEPTH_LEN); ++ if (loc != TS_NULL_MLOC) { ++ d = TSMimeHdrFieldValueUintGet(bufp, hdr_loc, loc, -1); ++ } ++ TSHandleMLocRelease(bufp, hdr_loc, loc); ++ if (d >= pOptionInfo->max_inclusion_depth) { ++ TSError("[esi][%s] The current esi inclusion depth (%u) is larger than or equal to the max (%u)", __FUNCTION__, d, ++ pOptionInfo->max_inclusion_depth); ++ return false; ++ } ++ + int method_len; + const char *method; + method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_len); +@@ -1341,7 +1385,7 @@ isTxnTransformable(TSHttpTxn txnp, bool + } + + static bool +-isCacheObjTransformable(TSHttpTxn txnp, bool *intercept_header, bool *head_only) ++isCacheObjTransformable(TSHttpTxn txnp, const OptionInfo *pOptionInfo, bool *intercept_header, bool *head_only) + { + int obj_status; + if (TSHttpTxnCacheLookupStatusGet(txnp, &obj_status) == TS_ERROR) { +@@ -1350,7 +1394,7 @@ isCacheObjTransformable(TSHttpTxn txnp, + } + if (obj_status == TS_CACHE_LOOKUP_HIT_FRESH) { + TSDebug(DEBUG_TAG, "[%s] doc found in cache, will add transformation", __FUNCTION__); +- return isTxnTransformable(txnp, true, intercept_header, head_only); ++ return isTxnTransformable(txnp, true, pOptionInfo, intercept_header, head_only); + } + TSDebug(DEBUG_TAG, "[%s] cache object's status is %d; not transformable", __FUNCTION__, obj_status); + return false; +@@ -1523,7 +1567,7 @@ globalHookHandler(TSCont contp, TSEvent + if (event == TS_EVENT_HTTP_READ_RESPONSE_HDR) { + bool mask_cache_headers = false; + TSDebug(DEBUG_TAG, "[%s] handling read response header event", __FUNCTION__); +- if (isTxnTransformable(txnp, false, &intercept_header, &head_only)) { ++ if (isTxnTransformable(txnp, false, pOptionInfo, &intercept_header, &head_only)) { + addTransform(txnp, true, intercept_header, head_only, pOptionInfo); + Stats::increment(Stats::N_OS_DOCS); + mask_cache_headers = true; +@@ -1536,7 +1580,7 @@ globalHookHandler(TSCont contp, TSEvent + } + } else { + TSDebug(DEBUG_TAG, "[%s] handling cache lookup complete event", __FUNCTION__); +- if (isCacheObjTransformable(txnp, &intercept_header, &head_only)) { ++ if (isCacheObjTransformable(txnp, pOptionInfo, &intercept_header, &head_only)) { + // we make the assumption above that a transformable cache + // object would already have a transformation. We should revisit + // that assumption in case we change the statement below +@@ -1589,7 +1633,7 @@ esiPluginInit(int argc, const char *argv + gHandlerManager = new HandlerManager(HANDLER_MGR_DEBUG_TAG, &TSDebug, &TSError); + } + +- memset(pOptionInfo, 0, sizeof(struct OptionInfo)); ++ new (pOptionInfo) OptionInfo; + + if (argc > 1) { + int c; +@@ -1599,11 +1643,12 @@ esiPluginInit(int argc, const char *argv + {const_cast("disable-gzip-output"), no_argument, nullptr, 'z'}, + {const_cast("first-byte-flush"), no_argument, nullptr, 'b'}, + {const_cast("handler-filename"), required_argument, nullptr, 'f'}, ++ {const_cast("max-inclusion-depth"), required_argument, nullptr, 'i'}, + {nullptr, 0, nullptr, 0}, + }; + + int longindex = 0; +- while ((c = getopt_long(argc, const_cast(argv), "npzbf:", longopts, &longindex)) != -1) { ++ while ((c = getopt_long(argc, const_cast(argv), "npzbf:i:", longopts, &longindex)) != -1) { + switch (c) { + case 'n': + pOptionInfo->packed_node_support = true; +@@ -1623,6 +1668,18 @@ esiPluginInit(int argc, const char *argv + gHandlerManager->loadObjects(handler_conf); + break; + } ++ case 'i': { ++ unsigned max; ++ auto num = std::sscanf(optarg, "%u", &max); ++ if (num != 1) { ++ TSEmergency("[esi][%s] value for maximum inclusion depth (%s) is not unsigned integer", __FUNCTION__, optarg); ++ } ++ if (max > 9) { ++ TSEmergency("[esi][%s] maximum inclusion depth (%s) large than 9", __FUNCTION__, optarg); ++ } ++ pOptionInfo->max_inclusion_depth = max; ++ break; ++ } + default: + break; + } +@@ -1632,9 +1689,9 @@ esiPluginInit(int argc, const char *argv + TSDebug(DEBUG_TAG, + "[%s] Plugin started, " + "packed-node-support: %d, private-response: %d, " +- "disable-gzip-output: %d, first-byte-flush: %d ", ++ "disable-gzip-output: %d, first-byte-flush: %d, max-inclusion-depth %u ", + __FUNCTION__, pOptionInfo->packed_node_support, pOptionInfo->private_response, pOptionInfo->disable_gzip_output, +- pOptionInfo->first_byte_flush); ++ pOptionInfo->first_byte_flush, pOptionInfo->max_inclusion_depth); + + return 0; + } +--- /dev/null ++++ trafficserver-9.2.5+ds/tests/gold_tests/pluginTest/esi/esi_nested_include.test.py +@@ -0,0 +1,137 @@ ++''' ++Test nested include for the ESI plugin. ++''' ++# Licensed to the Apache Software Foundation (ASF) under one ++# or more contributor license agreements. See the NOTICE file ++# distributed with this work for additional information ++# regarding copyright ownership. The ASF licenses this file ++# to you under the Apache License, Version 2.0 (the ++# "License"); you may not use this file except in compliance ++# with the License. You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++ ++import os ++ ++Test.Summary = ''' ++Test nested include for the ESI plugin. ++''' ++ ++Test.SkipUnless(Condition.PluginExists('esi.so'),) ++ ++ ++class EsiTest(): ++ """ ++ A class that encapsulates the configuration and execution of a set of ESI ++ test cases. ++ """ ++ """ static: The same server Process is used across all tests. """ ++ _server = None ++ """ static: A counter to keep the ATS process names unique across tests. """ ++ _ts_counter = 0 ++ """ static: A counter to keep any output file names unique across tests. """ ++ _output_counter = 0 ++ """ The ATS process for this set of test cases. """ ++ _ts = None ++ ++ def __init__(self, plugin_config): ++ """ ++ Args: ++ plugin_config (str): The config line to place in plugin.config for ++ the ATS process. ++ """ ++ if EsiTest._server is None: ++ EsiTest._server = EsiTest._create_server() ++ ++ self._ts = EsiTest._create_ats(self, plugin_config) ++ ++ @staticmethod ++ def _create_server(): ++ """ ++ Create and start a server process. ++ """ ++ # Configure our server. ++ server = Test.MakeOriginServer("server", lookup_key="{%uuid}") ++ ++ # Generate the set of ESI responses. ++ request_header = { ++ "headers": "GET /esi-nested-include.php HTTP/1.1\r\n" + "Host: www.example.com\r\n" + "Content-Length: 0\r\n\r\n", ++ "timestamp": "1469733493.993", ++ "body": "" ++ } ++ esi_body = r'''

++ ++

++''' ++ response_header = { ++ "headers": ++ "HTTP/1.1 200 OK\r\n" + "X-Esi: 1\r\n" + "Cache-Control: private\r\n" + "Content-Type: text/html\r\n" + ++ "Connection: close\r\n" + "Content-Length: {}\r\n".format(len(esi_body)) + "\r\n", ++ "timestamp": "1469733493.993", ++ "body": esi_body ++ } ++ server.addResponse("sessionfile.log", request_header, response_header) ++ ++ # Create a run to start the server. ++ tr = Test.AddTestRun("Start the server.") ++ tr.Processes.Default.StartBefore(server) ++ tr.Processes.Default.Command = "echo starting the server" ++ tr.Processes.Default.ReturnCode = 0 ++ tr.StillRunningAfter = server ++ ++ return server ++ ++ @staticmethod ++ def _create_ats(self, plugin_config): ++ """ ++ Create and start an ATS process. ++ """ ++ EsiTest._ts_counter += 1 ++ ++ # Configure ATS with a vanilla ESI plugin configuration. ++ ts = Test.MakeATSProcess("ts{}".format(EsiTest._ts_counter)) ++ ts.Disk.records_config.update({ ++ 'proxy.config.diags.debug.enabled': 1, ++ 'proxy.config.diags.debug.tags': 'http|plugin_esi', ++ }) ++ ts.Disk.remap_config.AddLine(f'map http://www.example.com/ http://127.0.0.1:{EsiTest._server.Variables.Port}') ++ ts.Disk.plugin_config.AddLine(plugin_config) ++ ++ ts.Disk.diags_log.Content = Testers.ContainsExpression( ++ r'The current esi inclusion depth \(3\) is larger than or equal to the max \(3\)', ++ 'Verify the ESI error concerning the max inclusion depth') ++ ++ # Create a run to start the ATS process. ++ tr = Test.AddTestRun("Start the ATS process.") ++ tr.Processes.Default.StartBefore(ts) ++ tr.Processes.Default.Command = "echo starting ATS" ++ tr.Processes.Default.ReturnCode = 0 ++ tr.StillRunningAfter = ts ++ return ts ++ ++ def run_test(self): ++ # Test 1: Verify basic ESI functionality without processing internal txn. ++ tr = Test.AddTestRun("First request") ++ tr.Processes.Default.Command = \ ++ ('curl http://127.0.0.1:{0}/main.php -H"Host: www.example.com" ' ++ '-H"Accept: */*" --verbose'.format( ++ self._ts.Variables.port)) ++ tr.Processes.Default.ReturnCode = 0 ++ tr.Processes.Default.Streams.stdout = "gold/nested_include_body.gold" ++ tr.StillRunningAfter = self._server ++ tr.StillRunningAfter = self._ts ++ ++ ++# ++# Configure and run the test cases. ++# ++ ++# Run the tests with ESI configured with private response. ++first_test = EsiTest(plugin_config='esi.so') ++first_test.run_test() +--- /dev/null ++++ trafficserver-9.2.5+ds/tests/gold_tests/pluginTest/esi/gold/nested_include_body.gold +@@ -0,0 +1,12 @@ ++

++

++

++

++ ++

++ ++

++ ++

++ ++

diff -Nru trafficserver-9.2.5+ds/debian/patches/series trafficserver-9.2.5+ds/debian/patches/series --- trafficserver-9.2.5+ds/debian/patches/series 2025-03-13 19:17:50.000000000 +0000 +++ trafficserver-9.2.5+ds/debian/patches/series 2025-06-23 16:05:59.000000000 +0000 @@ -6,3 +6,6 @@ 0021-workaround-obsolete-plantuml.patch 0022-workaround-missing-sphinxcontrib.jquery.patch 0023-update-to-929.patch +CVE-2024-53868.patch +CVE-2025-31698.patch +CVE-2025-49763.patch