CVE-2026-41940 & CVE-2026-31431: Two Critical Vulnerabilities Affecting Linux Servers – Fix Now

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 TierPatched Version
11.8611.86.0.41
11.11011.110.0.97
11.11811.118.0.63
11.12411.124.0.35
11.12611.126.0.54
11.13011.130.0.19
11.13211.132.0.29
11.13411.134.0.20
11.13611.136.0.5
WP Squared136.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:

SeverityMeaning
CRITICALActive exploitation confirmed. The injected session was used to authenticate successfully.
WARNINGSuspicious session structure that cannot occur in a legitimate flow.
ATTEMPTA failed exploit attempt was detected — session was probed but authentication was not achieved.
INFOAnomalous but inconclusive. Monitor and investigate.

The script also accepts the following optional flags:

FlagDescription
--verboseDump full contents of flagged session files
--purgeDelete all flagged session files and their preauth markers
--yesSkip interactive confirmation when used with --purge (required in cron/non-TTY environments)
--sessions-dir DIROverride the default /var/cpanel/sessions path
--access-log FILEOverride the default access log path

Exit codes (useful for monitoring integrations):

CodeMeaning
0Clean — no indicators found
1Probing activity detected (ATTEMPT or INFO only)
2Compromise 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:

  1. Purge all flagged session files (use --purge --yes with the detection script).
  2. Force a password reset for root and all WHM-level users.
  3. Audit /var/log/wtmp and WHM access logs for unauthorized logins.
  4. Check for persistence mechanisms: cron jobs, added SSH authorized keys, webshells, and any new system users.
  5. 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:

DistributionKernel Version Tested
Ubuntu 24.04 LTS6.17.0-1007-aws
Amazon Linux 20236.18.8-9.213.amzn2023
RHEL 14.36.12.0-124.45.1.el10_1
SUSE 166.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

Scroll to Top