Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
ZeroDays CVE-2026-49970
High CVE-2026-49970 CVSS 8.8 Laravel-Mediable PHP

Path Traversal in Laravel-Mediable Upload

CVE-2026-49970 is a path traversal flaw in Laravel-Mediable's sanitizePath() that lets attackers write files to arbitrary server locations, enabling RCE.

Offensive360 Research Team
Affects: < 7.0.0
Source Code View Patch

Overview

CVE-2026-49970 is a path traversal vulnerability in the File::sanitizePath() function of the Laravel-Mediable package, a widely adopted media management library for the Laravel PHP framework. The flaw exists in all versions prior to 7.0.0 and allows an attacker who controls the directory argument passed to MediaUploader::toDestination() to bypass the library’s sanitization logic and write uploaded files to arbitrary filesystem locations on the server.

The vulnerability stems from two compounding weaknesses: a regex character class that is too permissive — explicitly allowing both dot (.) and slash (/) characters — and a trim() call that only strips leading and trailing slashes from the final path string. When combined, these two mitigations fail to neutralize directory traversal sequences such as ../ embedded in the middle of an attacker-controlled path segment. The net result is that a file upload operation intended to land inside a designated storage disk can instead land in the web document root, overwrite environment configuration files like .env, or drop malicious PHP scripts into framework bootstrap directories.

Any Laravel application that exposes the MediaUploader::toDestination() directory parameter to user-supplied input — whether through a direct API argument, a URL query parameter, or an untrusted database value — is vulnerable. The CVSS 8.8 HIGH score reflects the high impact to confidentiality, integrity, and availability, with the attack requiring network access but no prior authentication in many typical deployment patterns where file upload endpoints are publicly accessible.

Technical Analysis

The root cause lives in src/FileHandling/File.php. The sanitizePath() method is responsible for normalizing directory paths before they are handed off to the filesystem abstraction layer. The vulnerable implementation looks substantially like the following:

// VULNERABLE — Laravel-Mediable < 7.0.0
// src/FileHandling/File.php

public static function sanitizePath(string $path): string
{
    // Strips any character that is NOT alphanumeric, underscore,
    // hyphen, dot, forward-slash, or space.
    // The inclusion of '.' and '/' makes traversal sequences possible.
    $path = preg_replace('/[^\w\s\-\.\/]/', '', $path);

    // Trims leading and trailing slashes — but does nothing
    // about traversal sequences embedded within the string.
    $path = trim($path, '/');

    return $path;
}

The critical mistake is in the character class [^\w\s\-\.\/]. By explicitly whitelisting . (dot) and / (forward slash), the regex preserves every component needed to construct a ../ traversal sequence. A caller that supplies a directory argument of uploads/../../public would see that string pass through sanitizePath() completely intact, because:

  1. Each . character is allowed by \. in the character class.
  2. Each / character is allowed by \/ in the character class.
  3. The trim($path, '/') call only removes slashes at position 0 and at the final position; it does not touch interior sequences.

The traversal then propagates to MediaUploader::toDestination(), which concatenates the sanitized directory with the filename and writes the file through Laravel’s Storage facade:

// Simplified call chain within MediaUploader
public function toDestination(string $disk, string $directory): static
{
    $this->disk = $disk;
    // sanitizePath() is called here, but it cannot neutralize
    // mid-string traversal sequences
    $this->directory = File::sanitizePath($directory);
    return $this;
}

// Later, during upload execution:
$path = $this->directory . '/' . $this->filename;
Storage::disk($this->disk)->put($path, $fileContents);

Because Storage::disk()->put() ultimately resolves against the disk’s configured root, a traversal sequence that escapes that root can write to any path the web-server process user has write permission on. On a typical Linux deployment where the application runs as www-data, that encompasses the entire web document root and often the application directory itself.

A minimal proof-of-concept HTTP request targeting a vulnerable upload endpoint might pass directory=uploads/../../public (URL-encoded as uploads%2F..%2F..%2Fpublic), causing an uploaded PHP file to land at public/shell.php — directly web-accessible and immediately executable.

Impact

An attacker who successfully exploits this vulnerability can achieve Remote Code Execution (RCE) by writing a PHP web shell to any directory within the web server’s document root. Beyond RCE, practical impact includes:

  • Environment file overwrite: Writing a crafted .env file to the application root to poison APP_KEY, database credentials, or mail server configuration.
  • Configuration tampering: Overwriting files in config/ or bootstrap/ to alter application behavior persistently across requests.
  • Sensitive data disclosure: If combined with a secondary read primitive, traversal knowledge reveals exact filesystem layout.
  • Supply chain pivot: On shared hosting or container environments, escaping the disk root can affect co-tenants.

The CVSS vector (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) captures the high-impact triad. Privilege Required is scored Low because most real-world upload endpoints require at minimum a logged-in user; deployments with unauthenticated upload endpoints would score this closer to 9.8.

How to Fix It

Upgrade immediately to Laravel-Mediable 7.0.0 via Composer:

composer require plank/laravel-mediable:^7.0.0

The patch in commit 6d1e7fb rewrites sanitizePath() to remove dot and slash from the allowed character class and adds an explicit traversal-sequence elimination step:

// FIXED — Laravel-Mediable 7.0.0
// src/FileHandling/File.php

public static function sanitizePath(string $path): string
{
    // Dots and slashes are no longer whitelisted individually.
    // Only word characters, hyphens, and spaces are retained.
    $path = preg_replace('/[^\w\s\-]/', '', $path);

    // Path segments are processed individually, eliminating any
    // remaining opportunity for traversal via separator injection.
    $segments = explode('/', $path);
    $segments = array_filter($segments, fn($s) => $s !== '' && $s !== '.' && $s !== '..');
    $path = implode('/', $segments);

    return $path;
}

If an immediate upgrade is not possible, apply the following interim controls:

  1. Validate directory at the controller layer before it reaches toDestination(). Reject any value containing . or / characters, or maintain a strict allowlist of permitted directories.
  2. Restrict filesystem permissions so the web-server process user cannot write outside the designated storage root.
  3. Disable PHP execution in all upload destination directories using .htaccess (php_flag engine off) or Nginx location blocks.

Our Take

Path traversal in file upload workflows is one of the most reliably recurring vulnerability classes in web application security, and this instance illustrates exactly why: sanitization by exclusion (stripping bad characters) is almost always weaker than sanitization by inclusion (allowing only a precisely defined safe set). The original regex tried to block the worst offenders while keeping flexibility for legitimate paths, but that flexibility was exactly what the attacker needed.

For enterprise teams, this vulnerability underscores the risk of trusting third-party media management packages with user-supplied path components without additional validation at the integration boundary. The package API design — accepting an arbitrary directory string — implicitly assumes the caller has already validated that input, an assumption that frequently does not hold in practice.

What should developers take away? Never pass unsanitized user input as a path component, even through a library that promises sanitization. Defense in depth means validating at the controller, sanitizing in the library, and enforcing filesystem permissions at the OS layer — all three, not one.

Detection with SAST

This vulnerability class maps to CWE-22: Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’). A well-tuned SAST engine will flag this through a combination of taint tracking and pattern analysis:

  • Taint source identification: Any Request::input(), $request->get(), or route parameter that flows into a string variable is marked as tainted.
  • Sink identification: Calls to Storage::put(), Storage::disk()->put(), file_put_contents(), and equivalent filesystem write sinks are registered as dangerous sinks.
  • Sanitizer modeling: The SAST engine models File::sanitizePath() as a proposed sanitizer. Because the regex in the vulnerable version retains . and /, the engine determines that the sanitizer does not neutralize the traversal taint and continues propagating it to the sink.
  • Traversal pattern matching: A secondary pattern rule flags regex character classes that simultaneously allow \. and \/ alongside string-to-path concatenation, regardless of taint flow — catching the class of bug even in non-obvious call graphs.

At Offensive360, our SAST ruleset specifically targets the “permissive allowlist” anti-pattern in PHP path sanitization functions, and would flag the original sanitizePath() implementation at code review time before a line of production traffic is ever served.

References

#path-traversal #file-upload #laravel #remote-code-execution

Detect this vulnerability class in your codebase

Offensive360 SAST scans your source code for CVE-2026-49970-class vulnerabilities and thousands of other patterns — across 60+ languages.