CVE-2026-23918 là một lỗ hổng (vulnerability) thuộc nhóm double free trong module mod_http2 của Apache HTTP Server. Lỗi xuất hiện trong quá trình xử lý và dọn dẹp HTTP/2 stream. Khi cùng một stream bị đưa vào hàng đợi cleanup nhiều hơn một lần, Apache có thể gọi hàm giải phóng bộ nhớ hai lần trên cùng một đối tượng.

Về mặt vận hành, hậu quả dễ quan sát nhất là worker process của Apache bị crash. Do đó, tác động trực tiếp là từ chối dịch vụ (Denial of Service). Trong một số điều kiện hẹp hơn, lỗi double free có thể được dùng làm điểm khởi đầu cho thực thi mã từ xa (Remote Code Execution). Tuy nhiên, RCE cần điều kiện heap, allocator và môi trường triển khai phù hợp. Vì vậy không nên đánh đồng mọi hệ thống Apache có HTTP/2 với một kịch bản RCE chắc chắn.

CVE: CVE-2026-23918

Thành phần: Apache HTTP Server, module mod_http2

Loại lỗi: CWE 415 Double Free

Điều kiện chính: HTTP/2 được bật

Hậu quả thực tế dễ thấy: worker process crash, dịch vụ mất ổn định

Hướng xử lý ưu tiên: nâng cấp bản vá hoặc tắt HTTP/2 tạm thời


Điểm cần nhấn mạnh là lỗi này không nằm ở logic ứng dụng web phía trên. Nó nằm ở tầng máy chủ web, cụ thể là tầng xử lý giao thức HTTP/2. Vì vậy ứng dụng PHP, Java, .NET hay static site phía sau Apache đều có thể bị ảnh hưởng nếu chúng phụ thuộc vào Apache làm reverse proxy hoặc web server có bật HTTP/2.

Điều kiện ảnh hưởng

Một hệ thống cần thỏa một số điều kiện trước khi có thể xem là nằm trong vùng rủi ro. Điều kiện quan trọng nhất là Apache đang dùng phiên bản có lỗi và module HTTP/2 đang được bật. Nếu HTTP/2 không được bật, đường xử lý lỗi này không được kích hoạt.

Cần phân biệt hai mức rủi ro. Mức thứ nhất là DoS. Đây là mức dễ xảy ra hơn vì chỉ cần làm cho worker process đi vào đường cleanup lỗi. Mức thứ hai là RCE. Mức này khó hơn nhiều vì attacker phải kiểm soát được trạng thái heap sau double free. Trong thực tế phòng thủ, tổ chức vẫn nên xử lý khẩn cấp vì DoS trên web server là đủ nghiêm trọng để ảnh hưởng dịch vụ.

Điều kiện đánh giá nhanh:

Apache chạy phiên bản bị ảnh hưởng
mod_http2 được bật
VirtualHost có khai báo h2
Dịch vụ public qua TLS và ALPN cho phép h2
Log có dấu hiệu worker process crash bất thường
Crash xuất hiện sau nhiều kết nối HTTP/2 ngắn và bị reset sớm

Vì sao HTTP/2 dễ tạo ra lỗi cleanup lặp

HTTP/2 cho phép nhiều stream chạy đồng thời trên cùng một kết nối TCP. Mỗi stream có vòng đời riêng. Khi client muốn hủy một stream, nó có thể gửi frame RST_STREAM. Server phải nhận frame này, cập nhật trạng thái stream và dọn tài nguyên liên quan.

Trong Apache, mod_http2 xử lý các sự kiện HTTP/2 thông qua callback. Một callback có thể được gọi khi nhận frame. Một callback khác có thể được gọi khi stream đóng. Nếu hai callback cùng nhìn thấy một stream cần cleanup, nhưng không có cơ chế đánh dấu stream đã được đưa vào cleanup queue, cùng một con trỏ stream có thể bị đưa vào queue hai lần.

Vấn đề ở đây không phải là việc có nhiều callback. Vấn đề nằm ở chỗ hàm cleanup phải có tính idempotent. Một thao tác idempotent có thể được gọi nhiều lần nhưng kết quả vẫn an toàn. Với lỗi này, cleanup không an toàn khi bị gọi lặp.

Đoạn code sau chỉ mô tả bản chất lỗi. Trong mô hình này, spurge là danh sách các stream chờ dọn dẹp. Hàm m_stream_cleanup đưa stream vào danh sách này. Nếu không kiểm tra stream đã có trong danh sách hay chưa, lỗi double free có thể xảy ra.

Logic lỗi như sau:

struct h2_stream {
    int id;
    void *pool;
};

struct h2_mplx {
    h2_stream *spurge[1024];
    int spurge_count;
};

void m_stream_cleanup(struct h2_mplx *m, struct h2_stream *s) {
    m->spurge[m->spurge_count] = s;
    m->spurge_count++;
}

void on_frame_recv_rst(struct h2_mplx *m, struct h2_stream *s) {
    m_stream_cleanup(m, s);
}

void on_stream_close(struct h2_mplx *m, struct h2_stream *s) {
    m_stream_cleanup(m, s);
}

void c1_purge_streams(struct h2_mplx *m) {
    for (int i = 0; i < m->spurge_count; i++) {
        h2_stream_destroy(m->spurge[i]);
    }
}

Nếu cùng một con trỏ s đi qua cả on_frame_recv_rst và on_stream_close, mảng spurge sẽ chứa hai phần tử trỏ đến cùng một đối tượng. Khi c1_purge_streams chạy, nó sẽ gọi h2_stream_destroy hai lần.

Trạng thái spurge sau khi lỗi xảy ra:

spurge[0] = 0x7f8c1000
spurge[1] = 0x7f8c1000

Lần destroy thứ nhất:
h2_stream_destroy(0x7f8c1000)
apr_pool_destroy(pool)
Kết quả: hợp lệ

Lần destroy thứ hai:
h2_stream_destroy(0x7f8c1000)
apr_pool_destroy(pool)
Kết quả: double free hoặc heap corruption

Logic an toàn:

Cách sửa tư duy là biến cleanup thành thao tác idempotent. Stream cần có trạng thái cho biết nó đã được đưa vào cleanup queue. Khi callback thứ hai chạy, nó nhìn thấy trạng thái này và bỏ qua.

struct h2_stream {
    int id;
    void *pool;
    int cleanup_scheduled;
};

void m_stream_cleanup_safe(struct h2_mplx *m, struct h2_stream *s) {
    if (s == NULL) {
        return;
    }

    if (s->cleanup_scheduled) {
        return;
    }

    s->cleanup_scheduled = 1;
    m->spurge[m->spurge_count] = s;
    m->spurge_count++;
}

Đây là điểm sinh viên cần chú ý khi đọc CVE dạng memory safety. Nhiều lỗi không bắt đầu từ một payload phức tạp. Nó bắt đầu từ một giả định sai trong code. Ở đây giả định sai là mỗi stream chỉ đi vào cleanup một lần.

Dấu hiệu trong Log

Khi lỗi bị kích hoạt ở mức DoS, dấu hiệu thường gặp là Apache worker process chết bất thường. Trên Linux, log có thể xuất hiện trong Apache error log, journald hoặc kernel log. Tên process có thể là apache2 trên Debian Ubuntu hoặc httpd trên RHEL CentOS.

Apache error logs:

[mpm_event:notice] child pid 13822 exit signal Aborted (6)
[mpm_event:notice] child pid 13829 exit signal Segmentation fault (11)
[core:notice] AH00052: child pid 13829 exit signal Segmentation fault (11)
[mpm_event:error] AH00484: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting

Không phải mọi dòng segmentation fault đều chắc chắn là CVE này. Tuy nhiên, nếu crash xuất hiện dày đặc, đi kèm HTTP/2 enabled và traffic có nhiều stream bị reset sớm, cần đưa CVE này vào danh sách kiểm tra.

Systemd journal:

apache2.service: Main process exited, code=killed, status=6/ABRT
apache2.service: Failed with result signal
apache2.service: Scheduled restart job, restart counter is at 4
apache2.service: Started The Apache HTTP Server

Kernel Logs:

kernel: apache2[13829]: segfault at 7f8c1000 ip 00007f8d2a1b4c20 sp 00007ffc21be9d60 error 4 in libc.so.6
kernel: traps: apache2[13829] general protection fault ip:7f8d2a1b4c20 sp:7ffc21be9d60 error:0

Access log gợi ý hành vi bất thường:

Access log HTTP truyền thống có thể không ghi rõ frame HTTP/2. Vì vậy không nên kỳ vọng access log cho biết trực tiếp có RST_STREAM. Tuy nhiên, có thể thấy dấu hiệu gián tiếp như rất nhiều kết nối ngắn, nhiều request không hoàn tất, hoặc nhiều request HTTP/2 từ cùng một nguồn trong thời gian ngắn.

198.51.100.23 - - [09/Jun/2026:10:12:01 +0700] GET / HTTP/2.0 200 0
198.51.100.23 - - [09/Jun/2026:10:12:01 +0700] GET / HTTP/2.0 200 0
198.51.100.23 - - [09/Jun/2026:10:12:02 +0700] GET / HTTP/2.0 200 0
198.51.100.23 - - [09/Jun/2026:10:12:02 +0700] GET / HTTP/2.0 200 0

Một chuỗi access log như trên chưa đủ để kết luận. Nó chỉ là tín hiệu để chuyển sang kiểm tra error log, journal, cấu hình HTTP/2 và core dump.

Kiểm tra hệ thống

Phần kiểm tra nên bắt đầu từ phiên bản Apache, module HTTP/2, cấu hình VirtualHost và log crash. Không cần chạy payload khai thác trên hệ thống production.

Kiểm tra phiên bản Apache: Nếu đang ở phiên bản bị ảnh hưởng, cần lập tức kiểm tra tiếp HTTP/2 có bật không.

Server version: Apache/2.4.66 (Debian)
Server built:   2026-04-30T00:00:00

Kiểm tra module HTTP/2: Nếu thấy http2_module, điều đó có nghĩa module đã được load. Bước tiếp theo là kiểm tra VirtualHost có thật sự cho phép giao thức h2 hay không.

apache2ctl -M | grep http2

httpd -M | grep http2
http2_module (shared)

Kiểm tra cấu hình Protocols:  

grep -R "Protocols" /etc/apache2/sites-enabled /etc/apache2/mods-enabled
grep -R "Protocols" /etc/httpd/conf.d /etc/httpd/conf.modules.d
/etc/apache2/sites-enabled/default-ssl.conf: Protocols h2 http/1.1

Dòng Protocols h2 http/1.1 cho thấy HTTP/2 đang được bật cho virtual host đó.

Kiểm tra log crash gần đây:

journalctl -u apache2 --since 2026-06-01 | grep -E "segfault|Aborted|exit signal|core"

journalctl -u httpd --since 2026-06-01 | grep -E "segfault|Aborted|exit signal|core"

Kiểm tra core dump nếu hệ thống cho phép

coredumpctl list | grep -E "apache2|httpd"

coredumpctl info apache2

coredumpctl info httpd

Nếu core dump chỉ ra crash tại vùng cleanup, destroy stream hoặc APR pool, mức nghi ngờ sẽ cao hơn. Tuy nhiên, việc phân tích core dump cần đội vận hành hoặc đội bảo mật có kinh nghiệm về native debugging.

Xử lý và giảm thiểu rủi ro

  • Cách xử lý đúng nhất là nâng cấp Apache lên phiên bản đã vá. Nếu chưa thể nâng cấp ngay, tắt HTTP/2 là biện pháp giảm thiểu trực tiếp vì nó loại bỏ đường xử lý lỗi.
  • Nâng cấp trên Debian Ubuntu: Sau khi nâng cấp, cần kiểm tra lại phiên bản, trạng thái service và error log.
  • Nâng cấp trên RHEL CentOS Fedora: 
  • Tắt HTTP/2 tạm thời: Trong trường hợp chưa có package vá hoặc chưa thể restart service theo quy trình thay đổi, có thể tạm thời loại bỏ h2 khỏi cấu hình Protocols.
  • Kiểm tra sau khi giảm thiểu: Nếu HTTP/2 đã bị tắt đúng, máy chủ không nên thương lượng h2 nữa. Trong thực tế, cần kiểm tra cả tầng load balancer, CDN hoặc reverse proxy phía trước, vì có hệ thống terminate HTTP/2 ở lớp ngoài rồi forward về Apache bằng HTTP/1.1.
  • Theo dõi sau xử lý: Nếu sau khi vá hoặc tắt HTTP/2 mà log crash dừng lại, khả năng cao rủi ro đã được xử lý. Nếu crash vẫn tiếp tục, cần điều tra nguyên nhân khác thay vì quy toàn bộ về CVE này.

Kết luận

CVE-2026-23918 là một ví dụ tốt về memory safety trong phần mềm hạ tầng. Lỗi không xuất phát từ nghiệp vụ ứng dụng, mà xuất phát từ cách một web server quản lý vòng đời của HTTP/2 stream. Một stream bị reset sớm có thể đi qua nhiều callback. Nếu cleanup không có trạng thái bảo vệ, cùng một đối tượng có thể bị giải phóng hai lần.

Về mặt phòng thủ, không nên chỉ đọc CVSS rồi kết luận. Cần kiểm tra phiên bản Apache, trạng thái HTTP/2, log crash, môi trường triển khai và khả năng vá. Với hệ thống production, ưu tiên là nâng cấp. Nếu chưa thể nâng cấp, tắt HTTP/2 là biện pháp giảm thiểu rõ ràng và dễ kiểm chứng.

Thông điệp quan trọng là cleanup trong các hệ thống xử lý bất đồng bộ phải được thiết kế an toàn khi gọi lặp. Trong code hệ thống, một callback chạy thêm một lần không được phép biến thành double free.