This is a bad loading dock, not a broken front door
CVE-2026-28780 is a heap overflow in mod_proxy_ajp that is triggered when Apache HTTP Server parses a malicious reply from an AJP backend. The key qualifier is the one the scary 9.8 summary hides: the attacker does not hit the public web listener directly and win; Apache must already be configured to use mod_proxy_ajp, and it must connect to a malicious or compromised AJP server that sends a crafted response. Apache says affected versions are through 2.4.66, fixed in 2.4.67 released on 2026-05-04.
The severity inflation comes from scoring this like a generic unauthenticated network RCE. Reality is narrower. Apache itself labeled it low on 2026-05-04/05, because the exploit path requires a trusted backend relationship, which usually means internal network reachability, a compromised app server, a rogue service registration, or a misrouted proxy target. That is still worth fixing because frontends often sit in higher-trust zones, but it is a post-initial-access or trust-boundary-pivot flaw, not an internet spray-and-pray emergency.
4 steps from start to impact.
Land behind the reverse proxy
- Apache is configured with
mod_proxy+mod_proxy_ajp - At least one
ajp://backend route is configured - The attacker can make Apache connect to a malicious AJP server
- Many modern deployments use HTTP/HTTPS or
mod_proxy_http, not AJP - AJP backends are usually internal-only and not attacker-controlled
- Backend allowlists, fixed IPs, and network segmentation reduce rogue-backend scenarios
proxy_ajp_module and ajp:// routes reliably; exposure scanners on the public internet are poor predictors because the vulnerable parser is on the frontend's backend connection.Send a crafted AJP reply
ajp_msg_check_header() and writes 4 attacker-controlled bytes past a heap buffer. That is memory corruption, but it is a very small overwrite and occurs in a constrained parsing path, so 'RCE' is possible in theory but not the default expectation in hardened builds.- AJP session established from Apache to attacker-controlled backend
- Backend can speak enough AJP to keep the proxy transaction alive
- A 4-byte overwrite is materially harder to weaponize than a large arbitrary write
- Allocator behavior, build flags, and platform hardening can turn this into a crash instead of code execution
- No credible public exploit chain was identified in reviewed sources
Corrupt the Apache worker
- The crafted overwrite lands in an exploitable heap state
- The target build and runtime make the corruption useful
- Prefork/worker/event model differences change exploit stability
- Modern Linux hardening and service supervision often collapse impact to DoS
- Apache child-process isolation limits one-shot blast radius
httpd/apache2 crashes or restarts. Standard web WAFs will not see the backend-originated malicious AJP response.Pivot from backend to frontend trust
- Successful memory-corruption exploitation or repeated crash condition
- Frontend process has access to sensitive configs, keys, or routing decisions
- Requires a prior foothold or trust abuse on the backend side
- Impact is usually limited to hosts that actually proxy over AJP
- No KEV listing and no reviewed evidence of in-the-wild exploitation
mod_proxy_ajp where unused — If the business does not require AJP, remove the module and eliminate the vulnerable code path entirely. For a MEDIUM verdict there is noisgate mitigation SLA: no mitigation SLA — go straight to the 365-day remediation window, but unused AJP should still be removed during normal config hygiene.8009 or your chosen port. This is the most practical containment if you must keep AJP and cannot remediate immediately.mod_proxy_http/HTTPS. That removes this parser family from future exposure and simplifies inspection and policy enforcement.httpd/apache2 worker exits, core dumps, and backend-specific request failures on AJP-connected frontends. It will not stop exploitation, but it gives you the best chance to catch unsuccessful or DoS-style attempts while patches are pending.- A public-edge WAF does not solve this, because the malicious payload comes back from the backend AJP server to Apache.
- MFA is irrelevant; there is no user authentication step in the exploit chain.
- Tomcat AJP
secrethelps authenticate intended peers, but it does not make a compromised legitimate backend harmless.
The supporting signals.
| In the wild | No reviewed source shows active exploitation. Not in CISA KEV as of this assessment. |
|---|---|
| Proof-of-concept availability | I found no authoritative public PoC referenced by Apache, NVD, or CISA-adjacent sources. Treat weaponization as *possible but not demonstrated* in the open sources reviewed. |
| EPSS | 0.00026 from the user-supplied intel block, which is consistent with a very low near-term exploitation probability. |
| KEV status | No. Absence from KEV matters here because this is exactly the kind of bug that looks terrifying in CVSS but has a much narrower operational path. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H assumes a generic network attacker. That overstates reachability because the attacker must control or compromise the AJP backend relationship, not merely send a web request to Apache. |
| Affected versions | Apache says through 2.4.66 are affected; fixed in 2.4.67. |
| Fixed versions and backports | Upstream fix is 2.4.67. Distros are backporting independently: Ubuntu shows fixes for 26.04/24.04/22.04, Debian tracks the issue, and SUSE shows released updates for several maintained package lines. |
| Exposure population | Internet scanning is a weak signal for this CVE. The vulnerable parser lives on Apache's backend AJP client path, so risk is concentrated in environments that still use mod_proxy_ajp and ajp:// routes. |
| Disclosure timeline | Published 2026-05-05; Apache 2.4.67 released 2026-05-04; NVD/CISA ADP attached the 9.8 vector on 2026-05-06. |
| Researchers | Credited by Apache to Andrew Lacambra, Elhanan Haenel, Tianshuo Han, and Tristan Madani. |
Sources.
Why this verdict
- Requires backend control: the exploit path starts only if Apache connects to a malicious AJP server, which implies internal position, prior compromise, or trust abuse.
- Reachable population is small: only hosts using
mod_proxy_ajpwithajp://backends are really in scope; plenty of Apache fleets never enable AJP at all. - Small overwrite, uncertain weaponization: the issue is a 4-byte attacker-controlled heap overwrite, which is serious but materially harder to convert into dependable RCE than the raw CVSS suggests.
Why not higher?
It is not higher because the public HTTP surface is not the real attack surface. The attacker must already be in a position to impersonate or compromise an AJP backend, which is compounding friction and a major reduction in exposed population. No KEV listing, no reviewed in-the-wild reports, and no authoritative public exploit chain push it further down.
Why not lower?
It is not lower because memory corruption inside a reverse proxy is never free. If you do run AJP, a compromised backend can attack the frontend process and potentially cross a trust boundary toward keys, routing logic, or adjacent applications. That keeps it out of pure backlog-hygiene territory.
Crowdsourced verification payload.
Run this on the target Apache host with local shell access. Invoke it as bash check_cve_2026_28780.sh or bash check_cve_2026_28780.sh /etc/apache2 if configs live in a nonstandard path; no root is required for version/module checks, but reading all config files may need elevated privileges.
#!/usr/bin/env bash
# check_cve_2026_28780.sh
# Conservative detector for CVE-2026-28780 on Apache HTTP Server.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
CFG_ROOT="${1:-}"
STATUS="UNKNOWN"
REASON=""
find_httpd_bin() {
local c
for c in apache2ctl apachectl httpd; do
if command -v "$c" >/dev/null 2>&1; then
command -v "$c"
return 0
fi
done
return 1
}
extract_version() {
local bin="$1"
local out ver
out="$($bin -v 2>/dev/null || true)"
ver="$(printf '%s\n' "$out" | sed -n 's/^Server version: Apache\/\([0-9][0-9.]*\).*$/\1/p' | head -n1)"
if [ -z "$ver" ]; then
ver="$(printf '%s\n' "$out" | sed -n 's/^Server version:[[:space:]]*Apache\/\([0-9][0-9.]*\).*$/\1/p' | head -n1)"
fi
printf '%s' "$ver"
}
ver_ge() {
# returns 0 if $1 >= $2
[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ]
}
ver_lt() {
# returns 0 if $1 < $2
! ver_ge "$1" "$2"
}
guess_cfg_roots() {
if [ -n "$CFG_ROOT" ] && [ -d "$CFG_ROOT" ]; then
printf '%s\n' "$CFG_ROOT"
fi
for d in /etc/apache2 /etc/httpd /usr/local/apache2/conf; do
[ -d "$d" ] && printf '%s\n' "$d"
done
}
module_loaded=0
ajp_configured=0
httpd_bin="$(find_httpd_bin || true)"
version=""
if [ -n "$httpd_bin" ]; then
version="$(extract_version "$httpd_bin")"
fi
# Check loaded modules if possible
if [ -n "$httpd_bin" ]; then
if "$httpd_bin" -M 2>/dev/null | grep -Eq 'proxy_ajp_module|mod_proxy_ajp'; then
module_loaded=1
fi
fi
# Fall back to config grep for module/config presence
while IFS= read -r root; do
if grep -R -Eqs 'LoadModule[[:space:]]+proxy_ajp_module|proxy_ajp_module' "$root" 2>/dev/null; then
module_loaded=1
fi
if grep -R -Eqs 'ajp://|ProxyPass[[:space:]].*ajp:|BalancerMember[[:space:]].*ajp:' "$root" 2>/dev/null; then
ajp_configured=1
fi
done < <(guess_cfg_roots)
if [ -z "$version" ]; then
echo "UNKNOWN - Could not determine Apache httpd version"
exit 2
fi
# Upstream fixed version
if ver_ge "$version" "2.4.67"; then
echo "PATCHED - Apache version $version is >= 2.4.67"
exit 0
fi
# Vulnerable code present in old version, but practical exposure depends on AJP use
if ver_lt "$version" "2.4.67"; then
if [ "$module_loaded" -eq 1 ] && [ "$ajp_configured" -eq 1 ]; then
echo "VULNERABLE - Apache version $version is < 2.4.67 and mod_proxy_ajp with ajp:// backend config was found"
exit 1
fi
if [ "$module_loaded" -eq 1 ] || [ "$ajp_configured" -eq 1 ]; then
echo "UNKNOWN - Apache version $version is < 2.4.67 and partial AJP indicators were found; verify distro backports and active config"
exit 2
fi
echo "UNKNOWN - Apache version $version is < 2.4.67 but no active AJP module/config was detected; package may still contain vulnerable code"
exit 2
fi
echo "UNKNOWN - Unable to classify"
exit 2