Back to Intelligence

CVE-2026-93352: Laravel-Mediable .pht Upload Bypass Enables PHP RCE on Apache — Detection and Remediation Guide

SA
Security Arsenal Team
September 23, 2026
10 min read

The NVD has published CVE-2026-93352, a CVSS 9.8 CRITICAL vulnerability affecting Laravel-Mediable versions 7.0.0 through 7.0.1 — a widely used Laravel package for handling media uploads. This is not a new bug class. It is an incomplete patch for CVE-2026-49972, and it carries a lesson every security engineer should internalize: a blocklist that is 99% complete is 0% effective.

The original fix for CVE-2026-49972 introduced a forbidden_extensions blocklist in config/mediable.php to prevent attackers from uploading executable PHP files. That blocklist includes phpt — but omits pht. On Debian and Ubuntu systems, Apache's default FilesMatch directive treats .pht as PHP and executes it. The result: an attacker uploads a .pht file, it sails through every validation check in MediaUploader::verifyExtension() and File::sanitizeFileName(), gets written to disk, and executes as PHP the moment it is requested over HTTP. Full remote code execution under the web server context, network-exploitable, no authentication nuances complicated enough to matter at CVSS 9.8.

If you run a Laravel application using Laravel-Mediable for user-facing uploads — avatars, documents, media galleries, CMS attachments — you should treat this as an emergency patch cycle item.

Technical Analysis

Affected Component and Versions

ItemDetail
CVECVE-2026-93352 (incomplete fix for CVE-2026-49972)
CVSS9.8 CRITICAL — Network vector, remotely exploitable
Affected productLaravel-Mediable
Affected versions7.0.0, 7.0.1 (fixed in 7.0.2)
Prerequisite platform conditionApache HTTP Server on Debian/Ubuntu with default FilesMatch PHP handler (.php, .pht, .phar, etc.)
Root causepht absent from forbidden_extensions blocklist in config/mediable.php

How the Attack Works — Defender's View of the Chain

  1. Reconnaissance: The attacker identifies an upload endpoint backed by Laravel-Mediable (profile photo, attachment, media manager). No special access is necessarily required beyond whatever the upload form itself requires.
  2. Payload crafting: A PHP web shell or arbitrary code payload is saved with the .pht extension instead of .php.
  3. Validation bypass: MediaUploader::verifyExtension() checks the extension against forbidden_extensions. Because pht is missing, the file passes. File::sanitizeFileName() likewise performs no rejection of the extension. The MIME and filename checks that would catch .php are blind to .pht.
  4. Write to disk: The file lands in the configured media disk — frequently a publicly web-accessible path (e.g., under storage/ symlinked via php artisan storage:link, or a public uploads directory).
  5. Execution: The attacker issues an HTTP GET to the uploaded file's URL. Apache's default PHP FilesMatch configuration on Debian/Ubuntu matches .pht, hands it to the PHP handler, and the payload executes with the web server user's privileges.
  6. Post-exploitation: From there, expect web shell command execution, credential harvesting from .env (database passwords, app keys), lateral movement, and persistence.

The critical amplifier here is the platform default. The Laravel-Mediable authors blocked what they thought were the executable variants, but Apache's Debian/Ubuntu PHP configuration has long treated .pht as executable. This is precisely why extension blocklists fail and why upload hardening must be defense-in-depth, not a single config array.

Exploitation Status

As of this writing, the NVD entry documents the vulnerability and its mechanics; there is no confirmed entry in CISA's Known Exploited Vulnerabilities catalog and no vendor-confirmed in-the-wild campaign cited in the advisory. Do not let that lower your urgency. The bypass is trivial — it requires no race conditions, no memory corruption, no special tooling. It is a one-character-class mistake with a public write-up in the NVD description itself. Historically, file-upload RCE flaws with this profile see proof-of-concept code within days of publication, and internet-facing Laravel applications are mass-scanned aggressively. Treat exploitation as imminent and hunt retroactively after patching.

Detection & Response

The most reliable detection surface for this vulnerability is the artifact itself: a .pht file appearing in a web-accessible upload path, and the web server subsequently executing it. The rules below are tuned for low noise — .pht files have essentially no legitimate business purpose in modern Laravel applications.

Sigma Rules

YAML
---
title: Laravel-Mediable .pht Webshell Upload in Web-Accessible Path
id: 8c2e1f47-3a9b-4d52-9e71-6f4a2b8c1d90
status: experimental
description: Detects creation of .pht files in common web upload, storage, or public directories. The .pht extension is executed as PHP by Apache's default FilesMatch directive on Debian/Ubuntu and is the bypass vector for CVE-2026-93352 in Laravel-Mediable 7.0.0-7.0.1.
references:
  - https://nvd.nist.gov/vuln/detail/CVE-2026-93352
  - https://attack.mitre.org/techniques/T1505/003/
author: Security Arsenal
date: 2026/04/06
tags:
  - attack.persistence
  - attack.t1505.003
logsource:
  category: file_event
  product: linux
detection:
  selection_extension:
    TargetFilename|endswith: '.pht'
  selection_paths:
    TargetFilename|contains:
      - '/var/www/'
      - '/storage/app/'
      - '/public/uploads/'
      - '/public/storage/'
      - '/uploads/'
      - '/media/'
  condition: selection_extension and selection_paths
falsepositives:
  - Extremely rare; .pht has no legitimate use in typical Laravel deployments
level: critical
---
title: Web Server Process Spawning Shell — Possible Webshell Execution
id: 2d7a9c14-6e3f-4b81-a5c2-9d1e3f7b4a06
status: experimental
description: Detects Apache or PHP-FPM worker processes spawning command shells or system utilities, consistent with post-exploitation activity following a successful .pht webshell upload via CVE-2026-93352.
references:
  - https://nvd.nist.gov/vuln/detail/CVE-2026-93352
  - https://attack.mitre.org/techniques/T1059/004/
author: Security Arsenal
date: 2026/04/06
tags:
  - attack.execution
  - attack.t1059.004
  - attack.t1505.003
logsource:
  category: process_creation
  product: linux
detection:
  selection_parent:
    ParentImage|endswith:
      - '/apache2'
      - '/httpd'
      - '/php-fpm'
      - '/php-fpm8.3'
      - '/php-fpm8.2'
      - '/php-fpm8.1'
  selection_child:
    Image|endswith:
      - '/sh'
      - '/bash'
      - '/dash'
      - '/curl'
      - '/wget'
      - '/nc'
      - '/ncat'
      - '/python'
      - '/python3'
      - '/perl'
  condition: selection_parent and selection_child
falsepositives:
  - Rare; some legacy admin panels or media-processing pipelines shell out from PHP — validate against application change records
level: high

KQL — Microsoft Sentinel / Defender

This query assumes Linux web server telemetry reaches Sentinel via Syslog/CEF ingestion (auditd file events) or via Defender for Endpoint onboarded Linux hosts. It hunts both the .pht artifact and web-server-spawned shell execution.

KQL — Microsoft Sentinel / Defender
// Hunt 1: .pht file creation in web-accessible paths (MDE-onboarded Linux hosts)
DeviceFileEvents
| where TimeGenerated > ago(14d)
| where FileName endswith ".pht"
| where FolderPath has_any ("/var/www", "/storage/app", "/public/uploads", "/public/storage", "/uploads", "/media")
| project TimeGenerated, DeviceName, FolderPath, FileName, InitiatingProcessFileName, InitiatingProcessCommandLine, SHA256
| order by TimeGenerated desc;

// Hunt 2: Web server or PHP-FPM spawning shells / download tools (webshell post-exploitation)
DeviceProcessEvents
| where TimeGenerated > ago(14d)
| where InitiatingProcessFileName has_any ("apache2", "httpd", "php-fpm")
| where FileName has_any ("sh", "bash", "dash", "curl", "wget", "nc", "ncat", "python", "python3", "perl")
| project TimeGenerated, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine, AccountName
| order by TimeGenerated desc;

// Hunt 3: Syslog/auditd file-creation telemetry via CEF/Syslog ingestion
Syslog
| where TimeGenerated > ago(14d)
| where SyslogMessage has ".pht"
| where SyslogMessage has_any ("/var/www", "storage/app", "uploads", "media")
| project TimeGenerated, Computer, ProcessName, SyslogMessage
| order by TimeGenerated desc

Velociraptor VQL

Use this artifact to sweep Linux web servers for dropped .pht payloads and enumerate recent writes under upload paths — useful both for proactive hunting and for retroactive scoping after patching.

VQL — Velociraptor
-- Hunt for .pht webshell artifacts in web-accessible upload directories (CVE-2026-93352)
SELECT FullPath, Size, Mtime, Ctime,
       hash(path=FullPath) AS FileHash
FROM glob(globs=[
  '/var/www/**/*.pht',
  '/var/www/**/storage/**/*.pht',
  '/var/www/**/uploads/**/*.pht',
  '/var/www/**/public/**/*.pht',
  '/srv/www/**/*.pht',
  '/home/*/public_html/**/*.pht'
])
ORDER BY Mtime DESC
VQL — Velociraptor
-- Identify files recently written by the web server user in upload paths (possible dropped payloads)
SELECT FullPath, Size, Mtime,
       stat(path=FullPath).Uid AS Uid
FROM glob(globs=['/var/www/**/uploads/**', '/var/www/**/storage/app/**'])
WHERE Mtime > now() - 1209600
  AND FullPath =~ '\\.(pht|phar|php[0-9]?|phtml)$'
ORDER BY Mtime DESC

Remediation / Verification Script (Bash)

Run this on Debian/Ubuntu Apache hosts serving Laravel applications. It inventories the installed Laravel-Mediable version, sweeps for existing .pht payloads, and applies the platform-level hardening that removes .pht execution independent of the application patch.

Bash / Shell
#!/usr/bin/env bash
# CVE-2026-93352 - Laravel-Mediable .pht upload bypass verification and hardening
set -euo pipefail

APP_ROOT="${1:-/var/www}"

echo "=== [1] Identify installed Laravel-Mediable versions ==="
find "$APP_ROOT" -maxdepth 4 -name composer.lock 2>/dev/null | while read -r lock; do
  ver=$(grep -A2 '"name": "plank/laravel-mediable"' "$lock" 2>/dev/null | grep '"version"' | head -1 || true)
  [ -n "$ver" ] && echo "  $lock -> $ver"
done

echo "=== [2] Sweep for existing .pht files (potential webshells) ==="
find "$APP_ROOT" /srv/www /home/*/public_html -type f -name '*.pht' -printf '%TY-%Tm-%Td %TH:%TM  %p\n' 2>/dev/null | sort -r | head -50 || echo "  none found"

echo "=== [3] Check Apache config for .pht in PHP FilesMatch handler ==="
grep -RniE 'FilesMatch.*\.pht|SetHandler.*php|AddHandler.*\.pht' /etc/apache2/ 2>/dev/null || echo "  no explicit .pht handler found in /etc/apache2 (check php module defaults)"

echo "=== [4] HARDEN: strip .pht (and other dangerous variants) from PHP execution ==="
cat > /etc/apache2/conf-available/disable-pht-execution.conf <<'EOF'
# CVE-2026-93352 hardening: never execute .pht as PHP
<FilesMatch "\.pht$">
    Require all denied
</FilesMatch>
# Belt-and-suspenders: deny script execution in upload/storage trees
<DirectoryMatch "/var/www/.*/(uploads|storage/app|public/storage)">
    <FilesMatch "\.(php|pht|phtml|phar|php[0-9])$">
        Require all denied
    </FilesMatch>
</DirectoryMatch>
EOF
a2enconf disable-pht-execution
apache2ctl configtest && systemctl reload apache2 && echo "  Apache hardened and reloaded"

echo "=== [5] Update Laravel-Mediable (run as the deploy user, per app) ==="
echo "  cd <app_root> && composer require plank/laravel-mediable:^7.0.2 && composer update plank/laravel-mediable"
echo "  Verify: grep -A2 'plank/laravel-mediable' composer.lock | grep version"

echo "=== [6] Verify blocklist now includes pht ==="
find "$APP_ROOT" -path '*/config/mediable.php' 2>/dev/null | while read -r cfg; do
  echo "  --- $cfg ---"
  grep -n 'forbidden_extensions' -A15 "$cfg" | grep -E "'pht'" && echo "  OK: pht present" || echo "  WARNING: pht MISSING from forbidden_extensions"
done

Remediation

1. Patch immediately. Upgrade Laravel-Mediable to 7.0.2 or later across every application that depends on it. Do not forget transitive usage — audit every composer.lock in your estate, including staging and forgotten legacy properties, which are disproportionately represented in breach postmortems:

Bash / Shell
find /var/www -name composer.lock -exec grep -l 'plank/laravel-mediable' {} \;
cd /path/to/app && composer require plank/laravel-mediable:^7.0.2

2. Apply the platform-level fix regardless of patch status. The Apache configuration hardening in the script above — denying execution of .pht and denying any PHP execution inside upload/storage directories — is the control that survives the next incomplete blocklist. Upload directories should be byte-storage, never execution contexts. This is the single most durable mitigation in this post.

3. Serve uploads from a non-executing path. Where architecture permits, move media delivery behind a controller that streams bytes with a safe Content-Type and Content-Disposition: attachment, or offload to object storage (S3-compatible) with a separate domain. An uploaded file that is never touched by the web server's PHP handler cannot become a webshell.

4. Retroactive scoping. After patching, assume prior exposure. Sweep for .pht files (script step 2 and the VQL artifact above), review web access logs for HTTP requests to .pht URLs (grep -E '\.pht' /var/log/apache2/*access*.log), and check for www-data-owned processes, unexpected crontab entries for the web user, and new files in writable directories modified in the exposure window. Any confirmed .pht execution should be treated as a full compromise: rotate .env credentials, database passwords, and application keys.

5. Longer-term control: prefer allowlists over blocklists for upload validation — enumerate the extensions you intend to accept (e.g., jpg, png, pdf) and reject everything else. CVE-2026-93352 exists precisely because blocklist maintenance is a losing game against platform defaults that vary by distribution and change over time.

6. Monitor for KEV inclusion and vendor updates. Track the NVD entry for CVE-2026-93352 and the Laravel-Mediable repository for the 7.0.2 advisory. If CISA adds this to the Known Exploited Vulnerabilities catalog, federal civilian agencies will face a binding remediation deadline — and your SLA should move accordingly regardless of sector.

The Bigger Lesson

This is the second CVE in this codebase for the same bug in a matter of weeks: the fix for CVE-2026-49972 was bypassed by a three-letter extension the authors didn't enumerate. When you patch CVE-2026-93352, don't close the ticket at composer update. Close it when the platform can no longer execute anything an attacker uploads — because CVE-2026-93352's successor will target whatever extension the 7.0.2 blocklist forgot next.

Related Resources

Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub

Is your security operations ready?

Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.