GitHub Security Advisory: GHSA-73gr-r64q-7jh4
Package: wwbn/avideo (COMPOSER)
Severity: MODERATE (CVSS 5.3)
CWE: CWE-863: Incorrect Authorization
Vulnerable versions: <= 26.0
Patched in: unpatched

Summary: AVideo has User Group-Based Category Access Control Bypass via Missing and Broken Group Filtering in categories.json.php

Details:
## Summary

The `categories.json.php` endpoint, which serves the category listing API, fails to enforce user group-based access controls on categories. In the default request path (no `?user=` parameter), user group filtering is entirely skipped, exposing all non-private categories including those restricted to specific user groups. When the `?user=` parameter is supplied, a type confusion bug causes the filter to use the admin user's (user_id=1) group memberships instead of the current user's, rendering the filter ineffective.

## Details

The vulnerability has two related failures in `objects/categories.json.php` and `objects/category.php`:

**1. Default request — group filtering completely skipped**

In `categories.json.php:17-24`, when `$_GET['user']` is not set, `$sameUserGroupAsMe` defaults to `false`:

```php
// categories.json.php:17-24
$onlyWithVideos = false;
$sameUserGroupAsMe = false;
if(!empty($_GET['user'])){
    $onlyWithVideos = true;
    $sameUserGroupAsMe = true;
}
$categories = Category::getAllCategories(true, $onlyWithVideos, false, $sameUserGroupAsMe);
```

In `category.php:438-452`, the user group filter is gated on `$sameUserGroupAsMe` being truthy:

```php
// category.php:438-452
if ($sameUserGroupAsMe) {
    $users_groups = UserGroups::getUserGroups($sameUserGroupAsMe);
    $users_groups_id = array(0);
    foreach ($users_groups as $value) {
        $users_groups_id[] = $value['id'];
    }
    $sql .= " AND ("
        . "(SELECT count(*) FROM categories_has_users_groups chug WHERE c.id = chug.categories_id) = 0 OR "
        . "(SELECT count(*) FROM categories_has_users_groups chug2 WHERE c.id = chug2.categories_id AND users_groups_id IN (" . implode(',', $users_groups_id) . ")) >= 1 "
        . ")";
}
```

Since `$sameUserGroupAsMe = false`, the entire block is skipped. All non-private categories are returned regardless of their user group restrictions set via the `categories_has_users_groups` table.

**2. With `?user=` parameter — boolean-to-integer type confusion**

When `$_GET['user']` is non-empty, `$sameUserGroupAsMe` is set to boolean `true` (line 21). This value is passed to `UserGroups::getUserGroups($sameUserGroupAsMe)` at `category.php:440`.

In `userGroups.php:349-379`, the parameter is used as `$users_id`:

```php
// userGroups.php:349,371,379
public static function getUserGroups($users_id){
    // ...
    $sql = "SELECT uug.*, ug.* FROM users_groups ug"
            . " LEFT JOIN users_has_users_groups uug ON users_groups_id = ug.id WHERE users_id = ? ";
    // ...
    $res = sqlDAL::readSql($sql, "i", [$users_id]);
```

PHP casts boolean `true` to integer `1` for the prepared statement bind, resulting in `WHERE users_id = 1` — fetching the **admin user's** group memberships. The filter then allows categories visible to admin groups, effectively granting any unauthenticated user the admin's category visibility.

**3. getTotalCategories also unfiltered**

`getTotalCategories()` at `category.php:978` does not ac