{"id":5611,"date":"2026-04-30T23:56:10","date_gmt":"2026-05-01T03:56:10","guid":{"rendered":"https:\/\/underhost.com\/blog\/?p=5611"},"modified":"2026-05-01T15:13:02","modified_gmt":"2026-05-01T19:13:02","slug":"critical-cpanel-authentication-bypass-patch-now-cve-2026-41940","status":"publish","type":"post","link":"https:\/\/underhost.com\/blog\/critical-cpanel-authentication-bypass-patch-now-cve-2026-41940\/","title":{"rendered":"Critical cPanel Authentication Bypass &#8211; Patch Now (CVE-2026-41940)"},"content":{"rendered":"<div class=\"uh-blog-hero\" style=\"margin-bottom: 40px; text-align: center; padding: 40px 24px; border-radius: 18px; background: linear-gradient(145deg, #0A1220, #0D1B30, #00D4FF); color: #ffffff; box-shadow: 0 20px 40px rgba(0, 212, 255, 0.15);\">\n<div style=\"display: inline-block; padding: 6px 14px; margin-bottom: 16px; border: 1px solid rgba(255,255,255,0.25); border-radius: 999px; color: #94a3b8; font-size: 0.9em; letter-spacing: 0.04em; text-transform: uppercase;\">Security Advisory \u00b7 CVE-2026-41940<\/div>\n<h2 style=\"color: #e0f2fe; font-size: 1.45em; line-height: 1.35; margin: 0 0 18px; font-weight: 500;\">CVE-2026-41940 affects cPanel &amp; WHM versions after 11.40 &#8211; Update to patched releases immediately<\/h2>\n<p style=\"font-size: 1.12em; color: #f8fafc; max-width: 760px; margin: 0 auto;\">UnderHost is releasing this urgent security guidance for server administrators, resellers, and self-managed VPS\/dedicated customers. A remote authentication flaw can allow attackers to bypass login controls. Patch versions are available &#8211; action required now.<\/p>\n<\/div>\n<p><!-- Excerpt \/ Lead --><\/p>\n<div class=\"uh-blog-excerpt\" style=\"border-left: 4px solid #00D4FF; padding: 18px 20px; margin: 0 0 32px; font-style: italic; background: #F8FAFC; color: #0d1b30; border-radius: 0 12px 12px 0; box-shadow: 0 10px 20px rgba(34, 211, 238, 0.08);\"><strong style=\"color: #0a1220;\">Critical severity &#8211; Authentication bypass in cPanel &amp; WHM (CVE-2026-41940).<\/strong><br \/>\nAll versions after 11.40 are vulnerable unless patched. UnderHost provides patched version numbers, firewall mitigations, a detection script, and immediate update instructions. <strong>Update before May 5, 2026, to avoid potential compromise.<\/strong><\/div>\n<hr style=\"border: 0; height: 1px; background: rgba(0, 212, 255, 0.20); margin: 32px 0;\" \/>\n<div class=\"uh-post-content\">\n<h2 style=\"color: #0a1220; font-size: 1.8em; margin-top: 34px;\"><i class=\"fas fa-shield-alt\" style=\"color: #00d4ff;\"><\/i> What is CVE-2026-41940?<\/h2>\n<p>CVE-2026-41940 is an <strong>authentication bypass vulnerability<\/strong> discovered in cPanel &amp; WHM (including DNSOnly). It affects all versions released after cPanel 11.40. An attacker can exploit this flaw to bypass authentication mechanisms, potentially gaining unauthorized access to WHM, cPanel accounts, and sensitive server functions. No public exploit is required &#8211; the vulnerability can be triggered remotely over ports 2083, 2087, 2095, and 2096.<\/p>\n<p><strong>Official advisory:<\/strong><br \/>\n<a style=\"color: #2563eb;\" href=\"https:\/\/support.cpanel.net\/hc\/en-us\/articles\/40073787579671-Security-CVE-2026-41940-cPanel-WHM-WP2-Security-Update-04-28-2026\" target=\"_blank\" rel=\"noopener noreferrer\"><br \/>\ncPanel Security CVE-2026-41940 \u2013 WP2 Security Update (04\/28\/2026)<br \/>\n<\/a><\/p>\n<ul>\n<li><strong>Impact:<\/strong> Unauthenticated remote attackers can bypass login pages and gain control of cPanel sessions.<\/li>\n<li><strong>Affected software:<\/strong> cPanel &amp; WHM, WP Squared (WP2) and DNSOnly versions after 11.40.<\/li>\n<li><strong>CVSSv3 Score:<\/strong> 9.8 (Critical) &#8211; low attack complexity, no privileges required.<\/li>\n<li><strong>UnderHost stance:<\/strong> All shared, reseller, and managed cloud nodes were patched within 6 hours. Self-managed customers must act now.<\/li>\n<\/ul>\n<h2 style=\"color: #0a1220; font-size: 1.8em; margin-top: 34px;\"><i class=\"fas fa-server\" style=\"color: #0091d6;\"><\/i> Patched Versions &amp; Required Updates<\/h2>\n<p>cPanel has released emergency patches for the following versions. <strong>If your server runs any cPanel version after 11.40 and is not listed below, update immediately.<\/strong><\/p>\n<p><!-- Highlight Box: Patch versions --><\/p>\n<blockquote style=\"background: #F8FAFC; border-left: 4px solid #00D4FF; padding: 20px; margin: 28px 0; border-radius: 0 12px 12px 0; box-shadow: 0 10px 20px rgba(34, 211, 238, 0.08);\">\n<h3 style=\"margin-top: 0; color: #0a1220;\">&#x2705; Patched cPanel &amp; WHM Versions<\/h3>\n<p style=\"margin-bottom: 8px; color: #0d1b30;\">11.86.0.41 \u2022 11.110.0.97 \u2022 11.118.0.63 \u2022 11.126.0.54 \u2022 11.130.0.19 \u2022 11.132.0.29 \u2022 11.136.0.5 \u2022 11.134.0.20<\/p>\n<p><strong>WP Squared (WP2) patched version:<\/strong> 136.1.7<\/p>\n<p><strong>Special case &#8211; CentOS 6 \/ CloudLinux 6 (v110.0.50):<\/strong> Upgrade to v110.0.103 via command below.<\/p>\n<p><code style=\"background: #0A1220; color: #ffffff; padding: 4px 10px; border-radius: 6px; display: inline-block;\">whmapi1 set_tier tier=11.110.0.103<\/code><\/p><\/blockquote>\n<h2 style=\"color: #0a1220; font-size: 1.8em; margin-top: 34px;\"><i class=\"fas fa-terminal\" style=\"color: #00d4ff;\"><\/i> Required Actions &#8211; Update Your Server NOW<\/h2>\n<p>Follow these steps exactly. Failure to patch leaves your server wide open to session injection and full account compromise.<\/p>\n<ol>\n<li><strong>Run cPanel update script as root:<\/strong><br \/>\n<code style=\"background: #0A1220; color: #fff; padding: 4px 8px; border-radius: 6px;\">\/scripts\/upcp --force<\/code><\/li>\n<li><strong>Verify the installed version:<\/strong><br \/>\n<code style=\"background: #0A1220; color: #fff; padding: 4px 8px; border-radius: 6px;\">\/usr\/local\/cpanel\/cpanel -V<\/code><br \/>\nConfirm output matches one of the patched versions above.<\/li>\n<li><strong>Restart cpsrvd (cPanel service) hard restart:<\/strong><br \/>\n<code style=\"background: #0A1220; color: #fff; padding: 4px 8px; border-radius: 6px;\">\/scripts\/restartsrv_cpsrvd --hard<\/code><\/li>\n<li><strong>If updates were pinned or disabled:<\/strong> Run <code>whmapi1 set_tier tier=11.110<\/code> (for CentOS 7\/CloudLinux 7). Review <a style=\"color: #2563eb;\" href=\"https:\/\/underhost.com\/support\/\">cPanel update preferences<\/a>.<\/li>\n<\/ol>\n<h2 style=\"color: #0a1220; font-size: 1.8em; margin-top: 34px;\"><i class=\"fas fa-fire-extinguisher\" style=\"color: #0091d6;\"><\/i> Mitigation (If You Cannot Patch Immediately)<\/h2>\n<p>Only use these temporary mitigations until you can run the full update. Both options block the exploitation path.<\/p>\n<ul>\n<li><strong>Firewall block:<\/strong> Drop inbound traffic on ports 2083, 2087, 2095, 2096.<\/li>\n<li><strong>Stop cpsrvd &amp; cpdavd (emergency only):<\/strong><br \/>\n<code style=\"background: #0A1220; color: #fff; padding: 4px 8px; border-radius: 6px;\">whmapi1 configureservice service=cpsrvd enabled=0 monitored=0 &amp;&amp; whmapi1 configureservice service=cpdavd enabled=0 monitored=0 &amp;&amp; \/scripts\/restartsrv_cpsrvd --stop &amp;&amp; \/scripts\/restartsrv_cpdavd --stop<\/code><\/li>\n<\/ul>\n<div class=\"uh-benefit-grid\" style=\"display: flex; flex-wrap: wrap; gap: 20px; justify-content: center; margin: 34px 0;\">\n<div style=\"flex: 1; min-width: 250px; background: #ffffff; padding: 26px; border-radius: 14px; border: 1px solid rgba(0, 212, 255, 0.20); border-top: 4px solid #00D4FF; box-shadow: 0 15px 30px rgba(34, 211, 238, 0.10);\">\n<h3 style=\"color: #0a1220; font-size: 1.2em; margin-top: 0; text-align: center;\">For Self-Managed VPS\/Dedicated<\/h3>\n<ul style=\"text-align: left; padding-left: 20px; color: #0d1b30;\">\n<li>Run the detection script immediately<\/li>\n<li>Apply cPanel updates using <code>\/scripts\/upcp --force<\/code><\/li>\n<li>Consider <a style=\"color: #2563eb;\" href=\"https:\/\/underhost.com\/cloud-vps.php\">UnderHost Managed VPS<\/a> for automatic patching<\/li>\n<\/ul>\n<\/div>\n<div style=\"flex: 1; min-width: 250px; background: #ffffff; padding: 26px; border-radius: 14px; border: 1px solid rgba(0, 212, 255, 0.20); border-top: 4px solid #0091D6; box-shadow: 0 15px 30px rgba(34, 211, 238, 0.10);\">\n<h3 style=\"color: #0a1220; font-size: 1.2em; margin-top: 0; text-align: center;\">For Resellers &amp; Agencies<\/h3>\n<ul style=\"text-align: left; padding-left: 20px; color: #0d1b30;\">\n<li>Verify all child servers are updated<\/li>\n<li>Block ports if any server cannot be patched<\/li>\n<li><a style=\"color: #2563eb;\" href=\"https:\/\/underhost.com\/reseller-hosting.php\">UnderHost Reseller plans<\/a> include proactive security<\/li>\n<\/ul>\n<\/div>\n<div style=\"flex: 1; min-width: 250px; background: #ffffff; padding: 26px; border-radius: 14px; border: 1px solid rgba(0, 212, 255, 0.20); border-top: 4px solid #94A3B8; box-shadow: 0 15px 30px rgba(34, 211, 238, 0.10);\">\n<h3 style=\"color: #0a1220; font-size: 1.2em; margin-top: 0; text-align: center;\">Shared Hosting Customers<\/h3>\n<ul style=\"text-align: left; padding-left: 20px; color: #0d1b30;\">\n<li>UnderHost already patched all shared nodes<\/li>\n<li>No action needed for your websites<\/li>\n<li><a style=\"color: #2563eb;\" href=\"https:\/\/underhost.com\/shared-hosting.php\">Upgrade to dedicated resources<\/a> for full control<\/li>\n<\/ul>\n<\/div>\n<\/div>\n<h2 style=\"color: #0a1220; font-size: 1.8em; margin-top: 34px;\"><i class=\"fas fa-search\" style=\"color: #00d4ff;\"><\/i> Detection Script &#8211; Check for Indicators of Compromise (IOC)<\/h2>\n<p>cPanel has released an official script to detect session injection artifacts. Run this immediately <strong>after<\/strong> patching to identify whether your server was exploited before the update.<\/p>\n<pre style=\"background: #0A1220; color: #e2e8f0; padding: 20px; border-radius: 14px; overflow-x: auto; font-size: 0.85em;\"><code>#!\/bin\/bash\r\n# Scan for compromised cPanel\/WHM session files.\r\n#\r\n# Each check function inspects a single session file and, if the IOC\r\n# matches, calls report_finding with a severity. report_finding records\r\n# the finding, prints a one-line header, and dumps the session for triage.\r\n# A summary of all findings (grouped by severity) is printed at the end.\r\n\r\n\r\n# Default paths\r\nSESSIONS_DIR=\"\/var\/cpanel\/sessions\"\r\nACCESS_LOG=\"\/usr\/local\/cpanel\/logs\/access_log\"\r\n\r\n# Flags\r\nVERBOSE=0\r\nPURGE=0\r\nASSUME_YES=0\r\n\r\n# Parse flags\r\nwhile [ $# -gt 0 ]; do\r\n    case \"$1\" in\r\n        --verbose)\r\n            VERBOSE=1\r\n            ;;\r\n        --purge)\r\n            PURGE=1\r\n            ;;\r\n        --yes|-y)\r\n            ASSUME_YES=1\r\n            ;;\r\n        --sessions-dir)\r\n            SESSIONS_DIR=\"$2\"; shift\r\n            ;;\r\n        --access-log)\r\n            ACCESS_LOG=\"$2\"; shift\r\n            ;;\r\n        --help|-h)\r\n            echo \"Usage: $0 [--verbose] [--purge [--yes]] [--sessions-dir DIR] [--access-log FILE]\"\r\n            exit 0\r\n            ;;\r\n        *)\r\n            echo \"Unknown argument: $1\" &gt;&amp;2\r\n            exit 1\r\n            ;;\r\n    esac\r\n    shift\r\ndone\r\n\r\n# Findings accumulator. Each entry: \"SEVERITY|session_file|short_message\"\r\nFINDINGS=()\r\n# Ordered list of unique session files that produced findings.\r\nFINDING_SESSIONS=()\r\n# Parallel array: token value associated with each entry in FINDING_SESSIONS\r\n# (first non-empty token seen for that session).\r\nFINDING_TOKENS=()\r\n# Parallel array: highest severity reported for each session (by index)\r\nFINDING_SEVERITIES=()\r\nCOUNT_CRITICAL=0\r\nCOUNT_WARNING=0\r\nCOUNT_INFO=0\r\nCOUNT_ATTEMPT=0\r\n\r\n# ---------------------------------------------------------------------------\r\n# Helpers\r\n# ---------------------------------------------------------------------------\r\n\r\n# Extract the value of a key=value line from a session file (first match).\r\n# Use: get_field  \r\nget_field() {\r\n    local file=\"$1\" key=\"$2\"\r\n    grep \"^${key}=\" \"$file\" | head -1 | cut -d= -f2-\r\n}\r\n\r\nhr() {\r\n    echo \"    ----------------------------------------------------------------\"\r\n}\r\n\r\n# Dump full contents of a session file plus related context (matching\r\n# pre-auth file, access_log hits for the injected token, file metadata).\r\n# Use: dump_session  [token_value]\r\ndump_session() {\r\n    local session_file=\"$1\"\r\n    local token_val=\"$2\"\r\n    local session_name preauth_file\r\n    session_name=$(basename \"$session_file\")\r\n    preauth_file=\"$SESSIONS_DIR\/preauth\/$session_name\"\r\n\r\n    hr\r\n    echo \"    SESSION DUMP: $session_file\"\r\n    hr\r\n    echo \"    File metadata:\"\r\n    ls -la \"$session_file\" 2&gt;\/dev\/null | sed 's\/^\/      \/'\r\n    echo\r\n    echo \"    Full session contents:\"\r\n    sed 's\/^\/      \/' \"$session_file\"\r\n    echo\r\n\r\n    if [ -f \"$preauth_file\" ]; then\r\n        echo \"    Matching pre-auth file: $preauth_file\"\r\n        ls -la \"$preauth_file\" 2&gt;\/dev\/null | sed 's\/^\/      \/'\r\n        echo \"    Pre-auth contents:\"\r\n        sed 's\/^\/      \/' \"$preauth_file\"\r\n        echo\r\n    fi\r\n\r\n    if [ -n \"$token_val\" ] &amp;&amp; [ -r \"$ACCESS_LOG\" ]; then\r\n        echo \"    Access log hits for token '$token_val':\"\r\n        grep -aF -- \"$token_val\" \"$ACCESS_LOG\" | sed 's\/^\/      \/' || echo \"      (none)\"\r\n        echo\r\n    fi\r\n    hr\r\n}\r\n\r\n# Record a finding and print a brief header line. The full session dump is\r\n# deferred to print_summary so that multiple findings for the same session\r\n# are grouped together and the session is only dumped once. When the same\r\n# session matches multiple IOCs at different severities, only the highest\r\n# (CRITICAL &gt; WARNING &gt; ATTEMPT &gt; INFO) is kept.\r\n# Use: report_finding    \r\n# SEVERITY is one of: CRITICAL, WARNING, ATTEMPT, INFO\r\nreport_finding() {\r\n    local severity=\"$1\"\r\n    local session_file=\"$2\"\r\n    local token_val=\"$3\"\r\n    local message=\"$4\"\r\n\r\n    # Severity ranking: CRITICAL=3, WARNING=2, ATTEMPT=1, INFO=0\r\n    local sev_rank=0\r\n    case \"$severity\" in\r\n        CRITICAL) sev_rank=3 ;;\r\n        WARNING)  sev_rank=2 ;;\r\n        ATTEMPT)  sev_rank=1 ;;\r\n        INFO)     sev_rank=0 ;;\r\n    esac\r\n\r\n    local i found=0 prev_sev prev_rank\r\n    for i in \"${!FINDING_SESSIONS[@]}\"; do\r\n        if [ \"${FINDING_SESSIONS[$i]}\" = \"$session_file\" ]; then\r\n            found=1\r\n            prev_sev=\"${FINDING_SEVERITIES[$i]}\"\r\n            case \"$prev_sev\" in\r\n                CRITICAL) prev_rank=3 ;;\r\n                WARNING)  prev_rank=2 ;;\r\n                ATTEMPT)  prev_rank=1 ;;\r\n                INFO)     prev_rank=0 ;;\r\n            esac\r\n            if [ \"$sev_rank\" -le \"$prev_rank\" ]; then\r\n                # Existing finding is at least as severe; ignore.\r\n                return\r\n            fi\r\n            # Upgrade in place: replace severity, token, FINDINGS entry,\r\n            # and roll back the previous severity counter so the new one\r\n            # can be incremented below without double-counting.\r\n            FINDING_SEVERITIES[$i]=\"$severity\"\r\n            [ -n \"$token_val\" ] &amp;&amp; FINDING_TOKENS[$i]=\"$token_val\"\r\n            local j\r\n            for j in \"${!FINDINGS[@]}\"; do\r\n                local entry=\"${FINDINGS[$j]}\"\r\n                local entry_sev=\"${entry%%|*}\"\r\n                local entry_file=\"${entry#*|}\"; entry_file=\"${entry_file%%|*}\"\r\n                if [ \"$entry_file\" = \"$session_file\" ] &amp;&amp; [ \"$entry_sev\" = \"$prev_sev\" ]; then\r\n                    FINDINGS[$j]=\"${severity}|${session_file}|${message}\"\r\n                    break\r\n                fi\r\n            done\r\n            case \"$prev_sev\" in\r\n                CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL - 1)) ;;\r\n                WARNING)  COUNT_WARNING=$((COUNT_WARNING - 1))   ;;\r\n                ATTEMPT)  COUNT_ATTEMPT=$((COUNT_ATTEMPT - 1))   ;;\r\n                INFO)     COUNT_INFO=$((COUNT_INFO - 1))         ;;\r\n            esac\r\n            break\r\n        fi\r\n    done\r\n\r\n    if [ \"$found\" -eq 0 ]; then\r\n        FINDING_SESSIONS+=(\"$session_file\")\r\n        FINDING_TOKENS+=(\"$token_val\")\r\n        FINDING_SEVERITIES+=(\"$severity\")\r\n        FINDINGS+=(\"${severity}|${session_file}|${message}\")\r\n    fi\r\n\r\n    case \"$severity\" in\r\n        CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL + 1)) ;;\r\n        WARNING)  COUNT_WARNING=$((COUNT_WARNING + 1))   ;;\r\n        ATTEMPT)  COUNT_ATTEMPT=$((COUNT_ATTEMPT + 1))   ;;\r\n        INFO)     COUNT_INFO=$((COUNT_INFO + 1))         ;;\r\n    esac\r\n\r\n    echo \"[${severity}] ${message}: ${session_file}\"\r\n}\r\n\r\n# ---------------------------------------------------------------------------\r\n# IOC checks\r\n# ---------------------------------------------------------------------------\r\n\r\n# IOC 0: token_denied counter alongside cp_security_token, in a session\r\n# whose origin is badpass or otherwise non-benign.\r\n#\r\n# - token_denied is incremented by do_token_denied() (cpsrvd.pl:3821)\r\n#   every time a request supplies the wrong cp_security_token. The\r\n#   session is killed on the third failure.\r\n# - cp_security_token itself is set by newsession() unconditionally\r\n#   while security tokens are enabled (Cpanel\/Server.pm:2290), so its\r\n#   presence is NOT by itself an IOC. The pair (token_denied,\r\n#   cp_security_token) tells us only that someone is actively trying\r\n#   tokens against this session.\r\n#\r\n# Auth markers (successful_*_auth_with_timestamp, hasroot=1,\r\n# tfa_verified=1, or an access_log hit on the security token) cannot\r\n# legitimately appear in a badpass session: the badpass call site\r\n# (Cpanel\/Server.pm:1244-1252) doesn't pass them, hasroot is not even\r\n# in _SESSION_PARTS (Cpanel\/Server.pm:2216-2247), and tfa_verified is\r\n# forced to 0 unless the caller passes a truthy value (line 2295).\r\n#\r\n# Severity tiers:\r\n#   CRITICAL - badpass origin AND auth markers present (post-exploit)\r\n#   INFO     - badpass origin, no auth markers, pass looks like a real\r\n#              encoded password (likely an unrelated failed login that\r\n#              happened to receive bad-token traffic)\r\n#   WARNING  - origin is neither badpass nor a known-benign method\r\n#              (handle_form_login, create_user_session,\r\n#              handle_auth_transfer); the suspicious origin itself is\r\n#              the IOC\r\n#\r\n# Legitimate badpass sessions never carry a pass= line (the badpass\r\n# call site at Cpanel\/Server.pm:1244-1252 does not pass `pass` to\r\n# newsession, and saveSession only writes pass= when length is\r\n# non-zero - Cpanel\/Session.pm:181). When we see one anyway we defer\r\n# classification to IOC 5 (check_failed_exploit_attempt), which flags\r\n# it as ATTEMPT.\r\ncheck_token_denied_with_injected_token() {\r\n    local session_file=\"$1\"\r\n\r\n    grep -q '^token_denied='      \"$session_file\" || return\r\n    grep -q '^cp_security_token=' \"$session_file\" || return\r\n\r\n    local token_val external_auth internal_auth hasroot tfa used\r\n    token_val=$(get_field      \"$session_file\" cp_security_token)\r\n    external_auth=$(get_field  \"$session_file\" successful_external_auth_with_timestamp)\r\n    internal_auth=$(get_field  \"$session_file\" successful_internal_auth_with_timestamp)\r\n    hasroot=$(get_field        \"$session_file\" hasroot)\r\n    tfa=$(get_field            \"$session_file\" tfa_verified)\r\n    used=\"\"\r\n    if [ -r \"$ACCESS_LOG\" ]; then\r\n        used=$(grep -aF -- \"$token_val\" \"$ACCESS_LOG\" | grep -m1 \" 200 \")\r\n    fi\r\n\r\n    local has_auth_markers=0\r\n    if [ -n \"$external_auth\" ] || [ -n \"$internal_auth\" ] \\\r\n       || [ \"$hasroot\" = \"1\" ] || [ \"$tfa\" = \"1\" ] || [ -n \"$used\" ]; then\r\n        has_auth_markers=1\r\n    fi\r\n\r\n    if grep -q '^origin_as_string=.*method=badpass' \"$session_file\"; then\r\n        if [ \"$has_auth_markers\" -eq 1 ]; then\r\n            report_finding CRITICAL \"$session_file\" \"$token_val\" \\\r\n                \"Exploitation artifact - token_denied with injected cp_security_token (badpass origin, token used)\"\r\n        else\r\n            # A pass= line on a badpass session is itself anomalous;\r\n            # defer to IOC 5 (ATTEMPT).\r\n            if grep -q '^pass=' \"$session_file\"; then\r\n                return\r\n            fi\r\n            report_finding INFO \"$session_file\" \"$token_val\" \\\r\n                \"Possible injected session (badpass origin, no usage observed)\"\r\n        fi\r\n    elif grep -q '^origin_as_string=.*method=handle_form_login' \"$session_file\" || \\\r\n         grep -q '^origin_as_string=.*method=create_user_session' \"$session_file\" || \\\r\n         grep -q '^origin_as_string=.*method=handle_auth_transfer' \"$session_file\"; then\r\n        # Known-benign origins where token_denied + cp_security_token\r\n        # genuinely happens during normal use.\r\n        return\r\n    else\r\n        report_finding WARNING \"$session_file\" \"$token_val\" \\\r\n            \"Suspicious session with token_denied + cp_security_token (non-badpass origin)\"\r\n    fi\r\n}\r\n\r\n# IOC 1: A session that still has its pre-auth marker file but already\r\n# contains an auth-success timestamp (external or internal).\r\n#\r\n# write_session creates $SESSIONS_DIR\/preauth\/ when the\r\n# session is written with needs_auth=1, and removes that marker once\r\n# needs_auth is cleared on promotion (Cpanel\/Session.pm:225-235). A\r\n# legitimately authenticated session therefore never has both the\r\n# preauth marker and an auth-success timestamp at the same time.\r\n#\r\n# Both successful_external_auth_with_timestamp and\r\n# successful_internal_auth_with_timestamp are checked: the original\r\n# poc.py payload injects the external variant; the watchtowr payload\r\n# (poc\/poc_watchtowr.py:35) injects the internal variant.\r\ncheck_preauth_with_auth_attrs() {\r\n    local session_file=\"$1\"\r\n    local session_name preauth_file\r\n    session_name=$(basename \"$session_file\")\r\n    preauth_file=\"$SESSIONS_DIR\/preauth\/$session_name\"\r\n\r\n    [ -f \"$preauth_file\" ] || return\r\n\r\n    local marker\r\n    if grep -qE '^successful_external_auth_with_timestamp=' \"$session_file\"; then\r\n        marker=\"successful_external_auth_with_timestamp\"\r\n    elif grep -qE '^successful_internal_auth_with_timestamp=' \"$session_file\"; then\r\n        marker=\"successful_internal_auth_with_timestamp\"\r\n    else\r\n        return\r\n    fi\r\n\r\n    report_finding CRITICAL \"$session_file\" \\\r\n        \"$(get_field \"$session_file\" cp_security_token)\" \\\r\n        \"Injected session - ${marker} present in pre-auth session\"\r\n}\r\n\r\n# IOC 2: tfa_verified=1 outside of a legitimate origin method.\r\n#\r\n# tfa_verified=1 is set in only two places:\r\n#   - Cpanel\/Security\/Authn\/TwoFactorAuth\/Verify.pm:122, after a real\r\n#     TFA token validation succeeds.\r\n#   - Cpanel\/Server.pm:2295, when a caller passes tfa_verified=1 to\r\n#     newsession().\r\n# In both cases the legitimate origin method is one of handle_form_login,\r\n# create_user_session, or handle_auth_transfer. tfa_verified=1 with any\r\n# other origin (notably badpass) cannot occur in a benign flow.\r\ncheck_tfa_with_bad_origin() {\r\n    local session_file=\"$1\"\r\n\r\n    grep -qE '^tfa_verified=1$' \"$session_file\" || return\r\n    grep -q '^origin_as_string=.*method=handle_form_login'    \"$session_file\" &amp;&amp; return\r\n    grep -q '^origin_as_string=.*method=create_user_session'  \"$session_file\" &amp;&amp; return\r\n    grep -q '^origin_as_string=.*method=handle_auth_transfer' \"$session_file\" &amp;&amp; return\r\n\r\n    report_finding WARNING \"$session_file\" \\\r\n        \"$(get_field \"$session_file\" cp_security_token)\" \\\r\n        \"Session with tfa_verified=1 but suspicious origin\"\r\n}\r\n\r\n# IOC 3: Session file contains a line that is not in `key=value` form.\r\n#\r\n# Three structural invariants together guarantee that every legitimate\r\n# line matches ^[A-Za-z_][A-Za-z0-9_]*=:\r\n#\r\n#   1. write_session serializes via Cpanel::Config::FlushConfig::flushConfig\r\n#      with '=' as the separator (Cpanel\/Session.pm:221), so the on-disk\r\n#      format is one key=value pair per line.\r\n#   2. Keys come from a fixed whitelist (_SESSION_PARTS at\r\n#      Cpanel\/Server.pm:2216-2247, applied at lines 2268-2270), so they\r\n#      always match the identifier shape above.\r\n#   3. Cpanel::Session::filter_sessiondata strips \\r\\n from every value\r\n#      (Cpanel\/Session.pm:315) and additionally strips \\r\\n=, from origin\r\n#      sub-values (line 312), so values can never re-introduce line\r\n#      breaks. The `pass` value is additionally encoded by saveSession\r\n#      (Cpanel\/Session.pm:181-189) into either lowercase hex (with-secret\r\n#      via Cpanel::Session::Encoder-&gt;encode_data) or the literal prefix\r\n#      `no-ob:` followed by lowercase hex (no-secret via\r\n#      Cpanel::Session::Encoder-&gt;hex_encode_only), so it cannot\r\n#      reintroduce structural characters either.\r\n#\r\n# Any non-blank line that fails the regex is the footprint of an\r\n# injection that bypassed these invariants - typically raw payload bytes\r\n# that didn't form valid key=value pairs. Note: an injection whose\r\n# smuggled lines DO match key=value (e.g. the watchtowr payload at\r\n# poc\/poc_watchtowr.py:35, which fabricates successful_internal_auth_\r\n# with_timestamp\/user\/tfa_verified\/hasroot lines) will not trip this\r\n# check; it is caught by IOC-0 and IOC-4 instead.\r\ncheck_malformed_session_line() {\r\n    local session_file=\"$1\"\r\n\r\n    # Look for any non-blank line that doesn't start with key=...\r\n    grep -nE -v '^[A-Za-z_][A-Za-z0-9_]*=|^[[:space:]]*$' \"$session_file\" &gt;\/dev\/null 2&gt;&amp;1 || return\r\n\r\n    report_finding CRITICAL \"$session_file\" \\\r\n        \"$(get_field \"$session_file\" cp_security_token)\" \\\r\n        \"Malformed session line(s) detected (not key=value - newline injection footprint)\"\r\n}\r\n\r\n# IOC 4: badpass origin combined with markers that no legitimate cpsrvd\r\n# code path writes into a badpass session.\r\n#\r\n# The badpass call site (Cpanel\/Server.pm:1244-1252) is:\r\n#\r\n#   $randsession = $self-&gt;newsession(\r\n#       'needs_auth' =&gt; 1,\r\n#       %security_token_options,            # adds cp_security_token\r\n#       'origin' =&gt; { 'method' =&gt; 'badpass' },\r\n#   );\r\n#\r\n# %security_token_options is why badpass sessions legitimately carry\r\n# cp_security_token, but no auth-related options are ever supplied.\r\n# newsession() filters %OPTS through the _SESSION_PARTS whitelist\r\n# (Cpanel\/Server.pm:2216-2247, applied at lines 2268-2270), so any key\r\n# not in that whitelist cannot land in the session via newsession at\r\n# all. Per marker:\r\n#\r\n#   successful_external_auth_with_timestamp - whitelisted, but the\r\n#       badpass caller doesn't pass it\r\n#   successful_internal_auth_with_timestamp - same\r\n#   tfa_verified=1 - newsession unconditionally writes 0 unless the\r\n#       caller passed a truthy value (Cpanel\/Server.pm:2295), and the\r\n#       badpass caller doesn't\r\n#   hasroot=1 - NOT in _SESSION_PARTS, so newsession cannot write it\r\n#       for ANY session. A repo-wide grep finds no caller of\r\n#       Cpanel::Session::Modify-&gt;set('hasroot', ...) either: hasroot is\r\n#       never written to a session by legitimate code. Its presence in\r\n#       any session file is conclusive evidence of newline injection\r\n#       (the watchtowr payload at poc\/poc_watchtowr.py:35 smuggles\r\n#       hasroot=1 via \\r\\n in a user-controlled field).\r\ncheck_badpass_with_auth_markers() {\r\n    local session_file=\"$1\"\r\n\r\n    grep -q '^origin_as_string=.*method=badpass' \"$session_file\" || return\r\n\r\n    local markers=()\r\n    grep -q '^successful_external_auth_with_timestamp=' \"$session_file\" \\\r\n        &amp;&amp; markers+=(\"successful_external_auth_with_timestamp\")\r\n    grep -q '^successful_internal_auth_with_timestamp=' \"$session_file\" \\\r\n        &amp;&amp; markers+=(\"successful_internal_auth_with_timestamp\")\r\n    grep -qE '^hasroot=1$'      \"$session_file\" &amp;&amp; markers+=(\"hasroot=1\")\r\n    grep -qE '^tfa_verified=1$' \"$session_file\" &amp;&amp; markers+=(\"tfa_verified=1\")\r\n\r\n    [ \"${#markers[@]}\" -gt 0 ] || return\r\n\r\n    local joined\r\n    joined=$(IFS=,; echo \"${markers[*]}\")\r\n    report_finding CRITICAL \"$session_file\" \\\r\n        \"$(get_field \"$session_file\" cp_security_token)\" \\\r\n        \"badpass origin combined with authenticated markers ($joined) - impossible in benign flow\"\r\n}\r\n\r\n# IOC 5: Failed exploit attempt - a badpass session that carries a\r\n# pass= line, a token_denied counter, and no auth markers.\r\n#\r\n# A legitimate badpass session is created at Cpanel\/Server.pm:1244-1252:\r\n#\r\n#   $randsession = $self-&gt;newsession(\r\n#       'needs_auth' =&gt; 1,\r\n#       %security_token_options,\r\n#       'origin' =&gt; { 'method' =&gt; 'badpass' },\r\n#   );\r\n#\r\n# %security_token_options carries only cp_security_token,\r\n# requested_token_at_next_login, and previous_session_user\r\n# (Cpanel\/Server.pm:1205-1226) - never `pass`. saveSession only\r\n# writes a pass= line when length($session_ref-&gt;{pass}) is non-zero\r\n# (Cpanel\/Session.pm:181), so legitimate badpass sessions have no\r\n# pass= line at all.\r\n#\r\n# An exploit that tampers with a user-controlled field on a\r\n# badpass-bound request leaves a pass= line behind (saveSession\r\n# encodes it as `` or `no-ob:` per Cpanel\/Session.pm:181-189,\r\n# but the format is irrelevant - its presence is the indicator). Combined\r\n# with token_denied (someone was poking at cp_security_token) and the\r\n# absence of auth markers (the injection didn't promote - otherwise\r\n# IOC-0 or IOC-4 fires CRITICAL), this is the signature of a failed\r\n# exploit attempt.\r\ncheck_failed_exploit_attempt() {\r\n    local session_file=\"$1\"\r\n\r\n    grep -q '^origin_as_string=.*method=badpass' \"$session_file\" || return\r\n    grep -q '^token_denied=' \"$session_file\" || return\r\n\r\n    # If auth markers are present, IOC-4 (CRITICAL) handles it.\r\n    grep -q '^successful_internal_auth_with_timestamp=' \"$session_file\" &amp;&amp; return\r\n    grep -q '^successful_external_auth_with_timestamp=' \"$session_file\" &amp;&amp; return\r\n\r\n    # Legitimate badpass sessions never carry pass=.\r\n    grep -q '^pass=' \"$session_file\" || return\r\n\r\n    report_finding ATTEMPT \"$session_file\" \"$(get_field \"$session_file\" cp_security_token)\" \\\r\n        \"Failed exploit attempt (badpass origin, token_denied, no auth markers, anomalous pass= line)\"\r\n}\r\n\r\n# ---------------------------------------------------------------------------\r\n# Main\r\n# ---------------------------------------------------------------------------\r\n\r\nscan_sessions() {\r\n    local session_file\r\n    while IFS= read -r -d '' session_file; do\r\n        check_token_denied_with_injected_token \"$session_file\"\r\n        check_preauth_with_auth_attrs          \"$session_file\"\r\n        check_tfa_with_bad_origin              \"$session_file\"\r\n        check_malformed_session_line           \"$session_file\"\r\n        check_badpass_with_auth_markers        \"$session_file\"\r\n        check_failed_exploit_attempt           \"$session_file\"\r\n    done &lt; &lt;(find \"$SESSIONS_DIR\/raw\" -type f -print0 2&gt;\/dev\/null)\r\n}\r\n\r\n\r\nprint_summary() {\r\n    local total=$((COUNT_CRITICAL + COUNT_WARNING + COUNT_INFO + COUNT_ATTEMPT))\r\n\r\n    echo\r\n    echo \"=================================================================\"\r\n    echo \"                       SCAN SUMMARY\"\r\n    echo \"=================================================================\"\r\n    echo \"  CRITICAL findings: $COUNT_CRITICAL\"\r\n    echo \"  WARNING  findings: $COUNT_WARNING\"\r\n    echo \"  ATTEMPT  findings: $COUNT_ATTEMPT\"\r\n    echo \"  INFO     findings: $COUNT_INFO\"\r\n    echo \"  Total            : $total\"\r\n    echo \"-----------------------------------------------------------------\"\r\n\r\n    if [ \"$total\" -eq 0 ]; then\r\n        echo \"[+] No indicators of compromise found.\"\r\n        return\r\n    fi\r\n\r\n    # --purge has destructive blast radius (live session files for every\r\n    # logged-in user). Require either --yes for non-interactive use, or\r\n    # an explicit \"yes\" at an attached TTY.\r\n    if [ \"$PURGE\" -eq 1 ] &amp;&amp; [ \"$ASSUME_YES\" -ne 1 ]; then\r\n        if [ ! -t 0 ]; then\r\n            echo \"[ERROR] --purge requires --yes when stdin is not a TTY (cron, pipes, etc)\" &gt;&amp;2\r\n            echo \"        Re-run with --yes to confirm deletion.\" &gt;&amp;2\r\n            exit 64\r\n        fi\r\n        echo\r\n        echo \"About to delete ${#FINDING_SESSIONS[@]} session file(s) plus matching preauth markers.\"\r\n        local confirm=\"\"\r\n        read -r -p \"Type 'yes' to confirm: \" confirm\r\n        if [ \"$confirm\" != \"yes\" ]; then\r\n            echo \"[+] Aborted; no files deleted.\"\r\n            PURGE=0\r\n        fi\r\n    fi\r\n\r\n\r\n    # For each unique session, print only the highest-severity finding, then dump\/purge as needed.\r\n    local i session token severity message found=0\r\n    for i in \"${!FINDING_SESSIONS[@]}\"; do\r\n        session=\"${FINDING_SESSIONS[$i]}\"\r\n        token=\"${FINDING_TOKENS[$i]}\"\r\n        severity=\"${FINDING_SEVERITIES[$i]}\"\r\n        found=0\r\n        # Find the first matching finding for this session and severity.\r\n        # Use `read` with three names so the last variable (entry_msg)\r\n        # absorbs any remaining `|` characters - the previous `${var##*|}`\r\n        # form took only the suffix after the LAST `|`, which would\r\n        # silently truncate any future message that contained one.\r\n        for entry in \"${FINDINGS[@]}\"; do\r\n            local entry_sev entry_file entry_msg\r\n            IFS='|' read -r entry_sev entry_file entry_msg &lt;&lt;&lt; \"$entry\" if [ \"$entry_file\" = \"$session\" ] &amp;&amp; [ \"$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\" &gt;&amp;2\r\n    echo \"        Pass --sessions-dir DIR to point at a different location\" &gt;&amp;2\r\n    echo \"        (the default is \/var\/cpanel\/sessions).\" &gt;&amp;2\r\n    exit 64\r\nfi\r\n\r\necho \"[*] Scanning session files for injection indicators...\"\r\nscan_sessions\r\nprint_summary\r\n\r\n# Exit codes (for cron \/ monitoring):\r\n#   2 - at least one CRITICAL or WARNING finding (compromise indicators)\r\n#   1 - only ATTEMPT or INFO findings (probing, no confirmed compromise)\r\n#   0 - clean scan\r\nif [ \"$COUNT_CRITICAL\" -gt 0 ] || [ \"$COUNT_WARNING\" -gt 0 ]; then\r\n    exit 2\r\nelif [ \"$COUNT_ATTEMPT\" -gt 0 ] || [ \"$COUNT_INFO\" -gt 0 ]; then\r\n    exit 1\r\nfi\r\nexit 0<\/code><\/pre>\n<p><strong>How to run:<\/strong> Save as <code>ioc_checksessions_files.sh<\/code>, then <code>\/bin\/bash .\/ioc_checksessions_files.sh<\/code>. Example output showing attack from IP 100.96.3.23 includes lines like <em>&#8220;CRITICAL: Exploitation artifact &#8211; token_denied with injected cp_security_token&#8221;<\/em>.<\/p>\n<p><strong>To make this easier for you, we updated the cPanel installation script slightly and hosted a mirror so it can now be downloaded and executed with one command line:<\/strong><\/p>\n<pre style=\"background: #0A1220; color: #e2e8f0; padding: 20px; border-radius: 14px; overflow-x: auto; font-size: 0.85em;\"><code>cd \/root &amp;&amp; wget -O cpanel.sh https:\/\/backup.underhost.com\/mirror\/cpanel.sh &amp;&amp; chmod +x cpanel.sh &amp;&amp; \/bin\/bash .\/cpanel.sh\r\n<\/code><\/pre>\n<h2 style=\"color: #0a1220; font-size: 1.8em; margin-top: 34px;\"><i class=\"fas fa-clock\" style=\"color: #0091d6;\"><\/i> What If My cPanel Version Isn\u2019t Listed?<\/h2>\n<p>Older unsupported versions (EOL) remain vulnerable with no patch. cPanel strongly recommends migrating to a supported release (11.118 or newer). UnderHost can assist with server migration and managed updates. Open a support ticket for personalized help.<\/p>\n<p><!-- CTA Section --><\/p>\n<div class=\"uh-pricing-cta\" style=\"background: linear-gradient(145deg, #0A1220, #0D1B30, #00D4FF); color: #ffffff; padding: 34px 24px; border-radius: 18px; text-align: center; margin: 44px 0; box-shadow: 0 20px 40px rgba(0, 212, 255, 0.15);\">\n<h2 style=\"margin-top: 0; font-size: 1.8em; color: #ffffff;\">Need Help Patching Your Server?<\/h2>\n<p style=\"font-size: 1.12em; margin-bottom: 22px; color: #e0f2fe;\">UnderHost Managed VPS and Dedicated Server clients receive automatic security updates. Contact our team for emergency patching assistance.<\/p>\n<p><a style=\"background: linear-gradient(90deg, #00D4FF, #0091D6); color: #0a1220; padding: 13px 30px; border-radius: 8px; text-decoration: none; font-weight: bold; display: inline-block; font-size: 1.05em; margin: 10px;\" href=\"https:\/\/underhost.com\/cloud-vps.php\"><br \/>\nView Managed VPS Plans<br \/>\n<\/a><\/p>\n<p><a style=\"background: transparent; color: #ffffff; padding: 12px 30px; border-radius: 8px; text-decoration: none; font-weight: bold; display: inline-block; font-size: 1.05em; margin: 10px; border: 2px solid rgba(255,255,255,0.75);\" href=\"https:\/\/customerpanel.ca\/client\/submitticket.php\"><br \/>\nOpen Emergency Support Ticket<br \/>\n<\/a><\/p>\n<\/div>\n<p><!-- FAQ Section --><\/p>\n<div class=\"uh-faq-section\" style=\"margin-bottom: 30px;\">\n<h2 style=\"color: #0a1220; font-size: 1.8em; border-bottom: 2px solid rgba(0, 212, 255, 0.20); padding-bottom: 10px;\">Frequently Asked Questions<\/h2>\n<div style=\"margin-top: 22px; background: #F8FAFC; padding: 18px 20px; border-radius: 12px; border: 1px solid rgba(34, 211, 238, 0.12);\">\n<h3 style=\"color: #0a1220; margin-top: 0;\">Q: Does this affect shared hosting customers on UnderHost?<\/h3>\n<p style=\"margin-bottom: 0; color: #0d1b30;\">No. UnderHost patched all shared, reseller, and managed cloud infrastructure on April 28, 2026, within hours of the patch release. Your websites are safe. Only self-managed VPS\/dedicated customers need to take action.<\/p>\n<\/div>\n<div style=\"margin-top: 18px; background: #F8FAFC; padding: 18px 20px; border-radius: 12px; border: 1px solid rgba(34, 211, 238, 0.12);\">\n<h3 style=\"color: #0a1220; margin-top: 0;\">Q: I cannot update right now &#8211; which ports should I block?<\/h3>\n<p style=\"margin-bottom: 0; color: #0d1b30;\">Block TCP inbound on ports <strong>2083, 2087, 2095, 2096<\/strong>. This prevents the authentication bypass from being reachable. However, updating is the only complete fix.<\/p>\n<\/div>\n<div style=\"margin-top: 18px; background: #F8FAFC; padding: 18px 20px; border-radius: 12px; border: 1px solid rgba(34, 211, 238, 0.12);\">\n<h3 style=\"color: #0a1220; margin-top: 0;\">Q: How do I verify my cPanel version after updating?<\/h3>\n<p style=\"margin-bottom: 0; color: #0d1b30;\">Run <code>\/usr\/local\/cpanel\/cpanel -V<\/code>. The output should match one of the patched versions listed above. Also check <code>\/scripts\/upcp --force<\/code> output for &#8220;already up to date&#8221;.<\/p>\n<\/div>\n<div style=\"margin-top: 18px; background: #F8FAFC; padding: 18px 20px; border-radius: 12px; border: 1px solid rgba(34, 211, 238, 0.12);\">\n<h3 style=\"color: #0a1220; margin-top: 0;\">Q: What if my detection script shows compromised sessions?<\/h3>\n<p style=\"margin-bottom: 0; color: #0d1b30;\">Immediately rotate ALL cPanel\/WHM passwords (root, resellers, every account). Purge the session directory: <code>rm -f \/var\/cpanel\/sessions\/raw\/*<\/code>. Audit logs for suspicious IPs and check for backdoors. UnderHost security team can assist with forensic investigation.<\/p>\n<\/div>\n<div style=\"margin-top: 18px; background: #F8FAFC; padding: 18px 20px; border-radius: 12px; border: 1px solid rgba(34, 211, 238, 0.12);\">\n<h3 style=\"color: #0a1220; margin-top: 0;\">Q: Does UnderHost offer managed patching for existing servers?<\/h3>\n<p style=\"margin-bottom: 0; color: #0d1b30;\">Yes. Any <a style=\"color: #2563eb;\" href=\"https:\/\/underhost.com\/cloud-vps.php\">UnderHost Cloud VPS or Dedicated Server<\/a> can be upgraded to fully managed support. Our team applies security patches, monitors CVEs, and performs updates during maintenance windows. Contact sales for a quote.<\/p>\n<\/div>\n<\/div>\n<hr style=\"border: 0; height: 1px; background: rgba(0, 212, 255, 0.20); margin: 32px 0;\" \/>\n<p style=\"font-size: 0.85em; color: #64748b; text-align: center;\"><i class=\"fas fa-history\"><\/i> Advisory updated: April 30, 2026 &#8211; includes restart instructions, API tier commands, detection script v2.<br \/>\nReferences: CVE-2026-41940, cPanel Security Team, UnderHost Security Bulletin #UH-2026-04.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Critical severity \u2013 Authentication bypass in cPanel &#038; WHM (CVE-2026-41940). All versions after 11.40 are vulnerable unless patched. UnderHost provides patched version numbers, firewall mitigations, a detection script, and immediate update instructions. Update before May 5, 2026, to avoid potential compromise.<\/p>\n","protected":false},"author":1,"featured_media":4786,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-5611","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-news"],"_links":{"self":[{"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/posts\/5611","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/comments?post=5611"}],"version-history":[{"count":8,"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/posts\/5611\/revisions"}],"predecessor-version":[{"id":5619,"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/posts\/5611\/revisions\/5619"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/media\/4786"}],"wp:attachment":[{"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/media?parent=5611"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/categories?post=5611"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/underhost.com\/blog\/wp-json\/wp\/v2\/tags?post=5611"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}