GitHub Security Advisory: GHSA-pw4v-x838-w5pg
Package: wwbn/avideo (COMPOSER)
Severity: HIGH (CVSS 7.5)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Vulnerable versions: <= 25.0
Patched in: unpatched

Summary: AVideo has an Authorization Bypass via Path Traversal in HLS Endpoint Allows Streaming Private/Paid Videos

Details:
## Summary

The HLS streaming endpoint (`view/hls.php`) is vulnerable to a path traversal attack that allows an unauthenticated attacker to stream any private or paid video on the platform. The `videoDirectory` GET parameter is used in two divergent code paths — one for authorization (which truncates at the first `/` segment) and one for file access (which preserves `..` traversal sequences) — creating a split-oracle condition where authorization is checked against one video while content is served from another.

## Details

The vulnerability is a split-oracle between the authorization lookup and the filesystem path construction. When `hls.php` receives a request, it processes `$_GET['videoDirectory']` through two independent functions that interpret the input differently.

**Step 1 — Authorization lookup truncates at first path segment** (`objects/video.php:1685-1688`):

```php
public static function getVideoFromFileName($fileName, $ignoreGroup = false, $ignoreTags = false)
{
    // ...
    $parts = explode("/", $fileName);
    if (!empty($parts[0])) {
        $fileName = $parts[0];  // Only takes first segment
    }
    $fileName = self::getCleanFilenameFromFile($fileName);
    // ...
    $sql = "SELECT id FROM videos WHERE filename = ? LIMIT 1";
    $res = sqlDAL::readSql($sql, "s", [$fileName]);
```

For input `public_video/../private_video`, `explode("/", ...)` yields `["public_video", "..", "private_video"]` and only `public_video` is used for the DB query. The authorization check at `hls.php:73` then runs against this public video:

```php
if (isAVideoUserAgent() || ... || User::canWatchVideo($video['id']) || ...) {
```

**Step 2 — File path construction preserves the traversal** (`objects/video.php:4622-4638`):

```php
public static function getPathToFile($videoFilename, $createDir = false)
{
    $videosDir = self::getStoragePath();
    $videoFilename = str_replace($videosDir, '', $videoFilename);
    $paths = Video::getPaths($videoFilename, $createDir);
    if (preg_match('/index(_offline)?.(m3u8|mp4|mp3)$/', $videoFilename)) {
        $paths['path'] = rtrim($paths['path'], DIRECTORY_SEPARATOR);
        $paths['path'] = rtrim($paths['path'], '/');
        $videoFilename = str_replace($paths['relative'], '', $videoFilename);
        $videoFilename = str_replace($paths['filename'], '', $videoFilename);
    }
    $newPath = addLastSlash($paths['path']) . "{$videoFilename}";
    $newPath = str_replace('//', '/', $newPath);
    return $newPath;
}
```

`getPaths` extracts the clean filename (e.g., `public_video`) to build the base path `/videos/public_video/`. Then `str_replace($paths['filename'], '', $videoFilename)` replaces only the clean name from the full input, leaving the traversal intact: `/../private_video/index.m3u8`. The concatenation at line 4634 produces `/videos/public_video/../private_video/index.m3u8`, which the OS resolves to `/videos/private_video/index.m3u8`.

**No mitigations exist in the path:**
- `fixPath()` (`objects/fun