Two critical security vulnerabilities were publicly disclosed in late April 2026 that affect a large portion of Linux-based servers, including cPanel/WHM installations and any Linux server running a kernel version from 4.14 onward. Both require immediate action.
This article covers what each vulnerability does, which systems are affected, and the exact steps to patch or mitigate each one.
CVE-2026-41940 — cPanel & WHM Authentication Bypass
What This Vulnerability Does
CVE-2026-41940 is an authentication bypass flaw in cPanel software, including DNSOnly. It affects all cPanel versions after 11.40. A remote attacker can exploit this to authenticate to cPanel, WHM, or Webmail interfaces without valid credentials, gaining full or partial access to hosted accounts and server administration panels.
Patches have been released. If your server is running cPanel, treat this as a P1 incident and update immediately.
Affected Versions
All cPanel & WHM versions from 11.40 onward are vulnerable. The following builds include the patch:
| cPanel Tier | Patched Version |
|---|---|
| 11.86 | 11.86.0.41 |
| 11.110 | 11.110.0.97 |
| 11.118 | 11.118.0.63 |
| 11.124 | 11.124.0.35 |
| 11.126 | 11.126.0.54 |
| 11.130 | 11.130.0.19 |
| 11.132 | 11.132.0.29 |
| 11.134 | 11.134.0.20 |
| 11.136 | 11.136.0.5 |
| WP Squared | 136.1.7 |
If you are on CentOS 6 or CloudLinux 6 running v110.0.50, a direct upgrade path to v110.0.103 is available (see Step 1 below).
How to Patch CVE-2026-41940
Step 1 — (CentOS 6 / CloudLinux 6 on v110 only) Set the upgrade tier before running the update:
whmapi1 set_tier tier=11.110.0.103
For CentOS 7 or CloudLinux 7 servers that need to pin to 11.110:
whmapi1 set_tier tier=11.110
Step 2 — Run the forced cPanel update:
/scripts/upcp --force
Step 3 — Confirm the build version after the update completes:
/usr/local/cpanel/cpanel -V
Verify the output matches one of the patched versions listed in the table above.
Step 4 — Perform a hard restart of the cPanel service:
/scripts/restartsrv_cpsrvd --hard
Note: If you have disabled automatic cPanel updates or pinned your server to a specific version, it will not have auto-updated. Identify and update these servers manually as a priority.
If You Cannot Update Immediately
Apply one of the following mitigations to reduce exposure while you prepare the update:
Option A — Block cPanel ports at the firewall:
Block inbound traffic on ports 2083, 2087, 2095, and 2096.
Option B — Stop the cPanel service entirely:
whmapi1 configureservice service=cpsrvd enabled=0 monitored=0 && \
whmapi1 configureservice service=cpdavd enabled=0 monitored=0 && \
/scripts/restartsrv_cpsrvd --stop && \
/scripts/restartsrv_cpdavd --stop
This will take cPanel and Webmail offline. Only use this if you have an alternative way to manage the server (SSH/WHM API) and downtime is acceptable.
Checking for Indicators of Compromise
cPanel has released a detection script that scans session files for signs of exploitation. Save the script below as ioc_checksessions_files.sh, then run it:
/bin/bash ./ioc_checksessions_files.sh
The script checks for six distinct exploitation patterns across session files in /var/cpanel/sessions/raw/ and produces a severity-graded summary:
=================================================================
SCAN SUMMARY
=================================================================
CRITICAL findings: 1
WARNING findings: 0
ATTEMPT findings: 1
INFO findings: 0
Total : 2
Severity levels:
| Severity | Meaning |
|---|---|
| CRITICAL | Active exploitation confirmed. The injected session was used to authenticate successfully. |
| WARNING | Suspicious session structure that cannot occur in a legitimate flow. |
| ATTEMPT | A failed exploit attempt was detected — session was probed but authentication was not achieved. |
| INFO | Anomalous but inconclusive. Monitor and investigate. |
The script also accepts the following optional flags:
| Flag | Description |
|---|---|
--verbose | Dump full contents of flagged session files |
--purge | Delete all flagged session files and their preauth markers |
--yes | Skip interactive confirmation when used with --purge (required in cron/non-TTY environments) |
--sessions-dir DIR | Override the default /var/cpanel/sessions path |
--access-log FILE | Override the default access log path |
Exit codes (useful for monitoring integrations):
| Code | Meaning |
|---|---|
0 | Clean — no indicators found |
1 | Probing activity detected (ATTEMPT or INFO only) |
2 | Compromise indicators detected (CRITICAL or WARNING present) |
Detection Script:
#!/bin/bash
# Scan for compromised cPanel/WHM session files.
SESSIONS_DIR="/var/cpanel/sessions"
ACCESS_LOG="/usr/local/cpanel/logs/access_log"
VERBOSE=0
PURGE=0
ASSUME_YES=0
while [ $# -gt 0 ]; do
case "$1" in
--verbose) VERBOSE=1 ;;
--purge) PURGE=1 ;;
--yes|-y) ASSUME_YES=1 ;;
--sessions-dir) SESSIONS_DIR="$2"; shift ;;
--access-log) ACCESS_LOG="$2"; shift ;;
--help|-h)
echo "Usage: $0 [--verbose] [--purge [--yes]] [--sessions-dir DIR] [--access-log FILE]"
exit 0 ;;
*) echo "Unknown argument: $1" >&2; exit 1 ;;
esac
shift
done
FINDINGS=()
FINDING_SESSIONS=()
FINDING_TOKENS=()
FINDING_SEVERITIES=()
COUNT_CRITICAL=0
COUNT_WARNING=0
COUNT_INFO=0
COUNT_ATTEMPT=0
get_field() { grep "^${2}=" "$1" | head -1 | cut -d= -f2-; }
hr() { echo " ----------------------------------------------------------------"; }
dump_session() {
local session_file="$1" token_val="$2" session_name preauth_file
session_name=$(basename "$session_file")
preauth_file="$SESSIONS_DIR/preauth/$session_name"
hr
echo " SESSION DUMP: $session_file"
hr
echo " File metadata:"
ls -la "$session_file" 2>/dev/null | sed 's/^/ /'
echo
echo " Full session contents:"
sed 's/^/ /' "$session_file"
echo
if [ -f "$preauth_file" ]; then
echo " Matching pre-auth file: $preauth_file"
ls -la "$preauth_file" 2>/dev/null | sed 's/^/ /'
echo " Pre-auth contents:"
sed 's/^/ /' "$preauth_file"
echo
fi
if [ -n "$token_val" ] && [ -r "$ACCESS_LOG" ]; then
echo " Access log hits for token '$token_val':"
grep -aF -- "$token_val" "$ACCESS_LOG" | sed 's/^/ /' || echo " (none)"
echo
fi
hr
}
report_finding() {
local severity="$1" session_file="$2" token_val="$3" message="$4"
local sev_rank=0
case "$severity" in CRITICAL) sev_rank=3 ;; WARNING) sev_rank=2 ;; ATTEMPT) sev_rank=1 ;; INFO) sev_rank=0 ;; esac
local i found=0 prev_sev prev_rank
for i in "${!FINDING_SESSIONS[@]}"; do
if [ "${FINDING_SESSIONS[$i]}" = "$session_file" ]; then
found=1
prev_sev="${FINDING_SEVERITIES[$i]}"
case "$prev_sev" in CRITICAL) prev_rank=3 ;; WARNING) prev_rank=2 ;; ATTEMPT) prev_rank=1 ;; INFO) prev_rank=0 ;; esac
if [ "$sev_rank" -le "$prev_rank" ]; then return; fi
FINDING_SEVERITIES[$i]="$severity"
[ -n "$token_val" ] && FINDING_TOKENS[$i]="$token_val"
local j
for j in "${!FINDINGS[@]}"; do
local entry="${FINDINGS[$j]}" entry_sev entry_file
entry_sev="${entry%%|*}"
entry_file="${entry#*|}"; entry_file="${entry_file%%|*}"
if [ "$entry_file" = "$session_file" ] && [ "$entry_sev" = "$prev_sev" ]; then
FINDINGS[$j]="${severity}|${session_file}|${message}"; break
fi
done
case "$prev_sev" in CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL-1)) ;; WARNING) COUNT_WARNING=$((COUNT_WARNING-1)) ;; ATTEMPT) COUNT_ATTEMPT=$((COUNT_ATTEMPT-1)) ;; INFO) COUNT_INFO=$((COUNT_INFO-1)) ;; esac
break
fi
done
if [ "$found" -eq 0 ]; then
FINDING_SESSIONS+=("$session_file")
FINDING_TOKENS+=("$token_val")
FINDING_SEVERITIES+=("$severity")
FINDINGS+=("${severity}|${session_file}|${message}")
fi
case "$severity" in CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL+1)) ;; WARNING) COUNT_WARNING=$((COUNT_WARNING+1)) ;; ATTEMPT) COUNT_ATTEMPT=$((COUNT_ATTEMPT+1)) ;; INFO) COUNT_INFO=$((COUNT_INFO+1)) ;; esac
echo "[${severity}] ${message}: ${session_file}"
}
check_token_denied_with_injected_token() {
local session_file="$1"
grep -q '^token_denied=' "$session_file" || return
grep -q '^cp_security_token=' "$session_file" || return
local token_val external_auth internal_auth hasroot tfa used
token_val=$(get_field "$session_file" cp_security_token)
external_auth=$(get_field "$session_file" successful_external_auth_with_timestamp)
internal_auth=$(get_field "$session_file" successful_internal_auth_with_timestamp)
hasroot=$(get_field "$session_file" hasroot)
tfa=$(get_field "$session_file" tfa_verified)
used=""
if [ -r "$ACCESS_LOG" ]; then
used=$(grep -aF -- "$token_val" "$ACCESS_LOG" | grep -m1 " 200 ")
fi
local has_auth_markers=0
if [ -n "$external_auth" ] || [ -n "$internal_auth" ] || [ "$hasroot" = "1" ] || [ "$tfa" = "1" ] || [ -n "$used" ]; then
has_auth_markers=1
fi
if grep -q '^origin_as_string=.*method=badpass' "$session_file"; then
if [ "$has_auth_markers" -eq 1 ]; then
report_finding CRITICAL "$session_file" "$token_val" "Exploitation artifact - token_denied with injected cp_security_token (badpass origin, token used)"
else
if grep -q '^pass=' "$session_file"; then return; fi
report_finding INFO "$session_file" "$token_val" "Possible injected session (badpass origin, no usage observed)"
fi
elif grep -q '^origin_as_string=.*method=handle_form_login' "$session_file" || \
grep -q '^origin_as_string=.*method=create_user_session' "$session_file" || \
grep -q '^origin_as_string=.*method=handle_auth_transfer' "$session_file"; then
return
else
report_finding WARNING "$session_file" "$token_val" "Suspicious session with token_denied + cp_security_token (non-badpass origin)"
fi
}
check_preauth_with_auth_attrs() {
local session_file="$1" session_name preauth_file
session_name=$(basename "$session_file")
preauth_file="$SESSIONS_DIR/preauth/$session_name"
[ -f "$preauth_file" ] || return
local marker
if grep -qE '^successful_external_auth_with_timestamp=' "$session_file"; then
marker="successful_external_auth_with_timestamp"
elif grep -qE '^successful_internal_auth_with_timestamp=' "$session_file"; then
marker="successful_internal_auth_with_timestamp"
else
return
fi
report_finding CRITICAL "$session_file" "$(get_field "$session_file" cp_security_token)" "Injected session - ${marker} present in pre-auth session"
}
check_tfa_with_bad_origin() {
local session_file="$1"
grep -qE '^tfa_verified=1$' "$session_file" || return
grep -q '^origin_as_string=.*method=handle_form_login' "$session_file" && return
grep -q '^origin_as_string=.*method=create_user_session' "$session_file" && return
grep -q '^origin_as_string=.*method=handle_auth_transfer' "$session_file" && return
report_finding WARNING "$session_file" "$(get_field "$session_file" cp_security_token)" "Session with tfa_verified=1 but suspicious origin"
}
check_malformed_session_line() {
local session_file="$1"
grep -nE -v '^[A-Za-z_][A-Za-z0-9_]*=|^[[:space:]]*$' "$session_file" >/dev/null 2>&1 || return
report_finding CRITICAL "$session_file" "$(get_field "$session_file" cp_security_token)" "Malformed session line(s) detected (not key=value - newline injection footprint)"
}
check_badpass_with_auth_markers() {
local session_file="$1"
grep -q '^origin_as_string=.*method=badpass' "$session_file" || return
local markers=()
grep -q '^successful_external_auth_with_timestamp=' "$session_file" && markers+=("successful_external_auth_with_timestamp")
grep -q '^successful_internal_auth_with_timestamp=' "$session_file" && markers+=("successful_internal_auth_with_timestamp")
grep -qE '^hasroot=1$' "$session_file" && markers+=("hasroot=1")
grep -qE '^tfa_verified=1$' "$session_file" && markers+=("tfa_verified=1")
[ "${#markers[@]}" -gt 0 ] || return
local joined; joined=$(IFS=,; echo "${markers[*]}")
report_finding CRITICAL "$session_file" "$(get_field "$session_file" cp_security_token)" "badpass origin combined with authenticated markers ($joined) - impossible in benign flow"
}
check_failed_exploit_attempt() {
local session_file="$1"
grep -q '^origin_as_string=.*method=badpass' "$session_file" || return
grep -q '^token_denied=' "$session_file" || return
grep -q '^successful_internal_auth_with_timestamp=' "$session_file" && return
grep -q '^successful_external_auth_with_timestamp=' "$session_file" && return
grep -q '^pass=' "$session_file" || return
report_finding ATTEMPT "$session_file" "$(get_field "$session_file" cp_security_token)" "Failed exploit attempt (badpass origin, token_denied, no auth markers, anomalous pass= line)"
}
scan_sessions() {
local session_file
while IFS= read -r -d '' session_file; do
check_token_denied_with_injected_token "$session_file"
check_preauth_with_auth_attrs "$session_file"
check_tfa_with_bad_origin "$session_file"
check_malformed_session_line "$session_file"
check_badpass_with_auth_markers "$session_file"
check_failed_exploit_attempt "$session_file"
done < <(find "$SESSIONS_DIR/raw" -type f -print0 2>/dev/null)
}
print_summary() {
local total=$((COUNT_CRITICAL+COUNT_WARNING+COUNT_INFO+COUNT_ATTEMPT))
echo
echo "================================================================="
echo " SCAN SUMMARY"
echo "================================================================="
echo " CRITICAL findings: $COUNT_CRITICAL"
echo " WARNING findings: $COUNT_WARNING"
echo " ATTEMPT findings: $COUNT_ATTEMPT"
echo " INFO findings: $COUNT_INFO"
echo " Total : $total"
echo "-----------------------------------------------------------------"
if [ "$total" -eq 0 ]; then echo "[+] No indicators of compromise found."; return; fi
if [ "$PURGE" -eq 1 ] && [ "$ASSUME_YES" -ne 1 ]; then
if [ ! -t 0 ]; then
echo "[ERROR] --purge requires --yes when stdin is not a TTY" >&2; exit 64
fi
echo
echo "About to delete ${#FINDING_SESSIONS[@]} session file(s) plus matching preauth markers."
local confirm=""
read -r -p "Type 'yes' to confirm: " confirm
if [ "$confirm" != "yes" ]; then echo "[+] Aborted; no files deleted."; PURGE=0; fi
fi
local i session token severity message found=0
for i in "${!FINDING_SESSIONS[@]}"; do
session="${FINDING_SESSIONS[$i]}"
token="${FINDING_TOKENS[$i]}"
severity="${FINDING_SEVERITIES[$i]}"
found=0
for entry in "${FINDINGS[@]}"; do
local entry_sev entry_file entry_msg
IFS='|' read -r entry_sev entry_file entry_msg <<< "$entry"
if [ "$entry_file" = "$session" ] && [ "$entry_sev" = "$severity" ]; then
message="$entry_msg"; found=1; break
fi
done
echo
echo "================================================================="
echo " SESSION: $session"
echo "================================================================="
echo " Findings:"
if [ "$found" -eq 1 ]; then printf " [%-8s] %s\n" "$severity" "$message"
else printf " [%-8s] %s\n" "$severity" "(no message found)"; fi
echo
if [ "$VERBOSE" -eq 1 ]; then dump_session "$session" "$token"; fi
if [ "$PURGE" -eq 1 ]; then
echo " [ACTION] Deleting session file: $session"
rm -f -- "$session"
local preauth_marker="$SESSIONS_DIR/preauth/$(basename "$session")"
if [ -e "$preauth_marker" ]; then
echo " [ACTION] Deleting preauth marker: $preauth_marker"
rm -f -- "$preauth_marker"
fi
fi
done
if [ "$COUNT_CRITICAL" -gt 0 ] || [ "$COUNT_WARNING" -gt 0 ]; then
echo
echo "[!] INDICATORS OF COMPROMISE DETECTED - IMMEDIATE ACTION REQUIRED"
echo " 1. Purge all affected sessions"
echo " 2. Force password reset for root and all WHM users"
echo " 3. Audit /var/log/wtmp and WHM access logs for unauthorized access"
echo " 4. Check for persistence mechanisms (cron, SSH keys, backdoors)"
fi
}
if [ ! -d "$SESSIONS_DIR/raw" ]; then
echo "[ERROR] Sessions directory not found: $SESSIONS_DIR/raw" >&2
echo " Pass --sessions-dir DIR to point at a different location" >&2
exit 64
fi
echo "[*] Scanning session files for injection indicators..."
scan_sessions
print_summary
if [ "$COUNT_CRITICAL" -gt 0 ] || [ "$COUNT_WARNING" -gt 0 ]; then exit 2
elif [ "$COUNT_ATTEMPT" -gt 0 ] || [ "$COUNT_INFO" -gt 0 ]; then exit 1
fi
exit 0
If the Server Is Confirmed Compromised
If the detection script returns CRITICAL or WARNING findings, the server should be treated as root-compromised until proven otherwise.
Immediate actions:
- Purge all flagged session files (use
--purge --yeswith the detection script). - Force a password reset for
rootand all WHM-level users. - Audit
/var/log/wtmpand WHM access logs for unauthorized logins. - Check for persistence mechanisms: cron jobs, added SSH authorized keys, webshells, and any new system users.
- If you cannot confirm the scope of the compromise, migrate all cPanel accounts to a clean server or reinstall the OS and restore from a known-good backup.
CVE-2026-31431 — Linux Kernel “Copy Fail” Local Privilege Escalation
What This Vulnerability Does
CVE-2026-31431, nicknamed “Copy Fail,” is a local privilege escalation vulnerability in the Linux kernel. Any unprivileged local user — including users inside containers — can use it to obtain a root shell. It was introduced with kernel 4.14 and affects all major Linux distributions that have shipped a kernel in that range since 2017, including Ubuntu, Amazon Linux, RHEL, and SUSE.
The flaw is in the authencesn cryptographic module (algif_aead.c), reachable via the AF_ALG socket interface combined with the splice() system call. Unlike earlier kernel privilege escalation exploits, it requires no race condition, no kernel offsets, no recompilation, and no compiled payload. A 732-byte Python script using only standard library modules achieves reliable root on affected systems.
The mechanism: by splicing a file into a pipe and feeding it into an AF_ALG socket, the kernel’s page cache pages are placed into a writable destination scatterlist. The authencesn algorithm then writes 4 bytes past the declared output boundary directly into chained page cache pages. Because the kernel never marks those corrupted pages dirty for writeback, the on-disk file is untouched — standard file integrity tools that check on-disk checksums will not detect the modification. The attacker then executes the corrupted in-memory version of a setuid binary (such as /usr/bin/su) to obtain a root shell.
This vulnerability also functions as a Kubernetes container escape primitive, since the page cache is shared across all processes on a host, including across container boundaries.
Confirmed affected distributions:
| Distribution | Kernel Version Tested |
|---|---|
| Ubuntu 24.04 LTS | 6.17.0-1007-aws |
| Amazon Linux 2023 | 6.18.8-9.213.amzn2023 |
| RHEL 14.3 | 6.12.0-124.45.1.el10_1 |
| SUSE 16 | 6.12.0-160000.9-default |
How to Patch CVE-2026-31431
Step 1 — Apply the kernel update through your distribution’s package manager.
On Debian/Ubuntu:
apt update && apt upgrade -y linux-image-$(uname -r)
reboot
On RHEL/AlmaLinux/Rocky Linux:
dnf update kernel -y
reboot
On Amazon Linux 2023:
dnf update kernel -y
reboot
On SUSE:
zypper update -y kernel-default
reboot
Step 2 — Verify the kernel version after reboot:
uname -r
Cross-reference the output against your distribution’s security advisory to confirm the patched build is running.
Immediate Mitigation (If You Cannot Reboot Now)
Disable the algif_aead kernel module to remove the attack surface without rebooting:
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null
This prevents the module from loading or being loaded on demand. The second command unloads it if it is currently loaded. The change persists across reboots via the modprobe.d configuration.
Note: This mitigation blocks the exploit path but does not fix the underlying kernel vulnerability. Apply the kernel update and reboot as soon as your maintenance window allows.
Prevention
These two vulnerabilities highlight two recurring issues worth addressing systematically.
For cPanel servers:
- Keep automatic cPanel updates enabled. If you have pinned a specific version or disabled updates, document why and put a recurring review date on it. Pinned versions will not receive security patches automatically.
- Restrict access to cPanel ports (2083, 2087, 2095, 2096) at the firewall to known IP ranges where operationally possible.
- Set up WHM’s Two-Factor Authentication for all reseller and root-level accounts.
For Linux servers:
- Apply kernel updates within your defined patch SLA. For critical kernel CVEs, that window should be 24–72 hours with a mitigation applied immediately.
- Use a file integrity monitoring tool (AIDE, Wazuh, or similar) configured to check in-memory state, not just on-disk checksums, where supported.
- Limit local user accounts on shared systems. CVE-2026-31431 requires unprivileged local access — the fewer local users, the smaller the attack surface.
- On container platforms, ensure kernel updates are applied to the host node, not just inside containers.
Conclusion
CVE-2026-41940 is a remotely exploitable authentication bypass in cPanel affecting all versions since 11.40 — update via /scripts/upcp --force and run the detection script to confirm no exploitation has occurred. CVE-2026-31431 is a locally exploitable kernel privilege escalation affecting virtually every Linux system since 2017 — update the kernel and disable algif_aead as an immediate stopgap.
If you are running a Veeble cPanel Hosting or VPS plan and need assistance applying these patches, open a support ticket from your Veeble Client Zone and our team will help you through the update process.
References
- Security: CVE-2026-41940 – cPanel & WHM / WP2 Security Update 04/28/2026 — Official cPanel advisory with patched versions, detection script, and mitigation steps.
- Linux Kernel 0-Day “Copy Fail” Roots Every Major Distribution Since 2017 — Full technical disclosure of CVE-2026-31431, including exploit mechanism and affected distributions.