Skip to content

Commit 992b738

Browse files
committed
πŸ”’ Harden external image zoom endpoint against SSRF and open reflection
- πŸ›‘οΈ Require an authenticated operator session before rendering the `modal=external` image zoom view, blocking anonymous abuse of the reflection endpoint. - βœ… Sanitize the `src` parameter with `erLhcoreClassChatValidator::sanitizeUrl()` so only http/https URLs are accepted (drops gopher/ftp/mailto/...). - 🌐 Resolve the target host and reject loopback, private, and reserved IP ranges (e.g. 169.254.169.254) to prevent client-side SSRF probing of internal services. - 🚫 Refuse localhost and unresolvable hostnames so attacker-controlled URLs are never reflected into `<img src>`. - πŸ’₯ Return proper HTTP error codes (400/403) instead of silently rendering invalid input. - ✨ Render the sanitized URL in the template rather than the raw `$_GET['src']` value.
1 parent fada146 commit 992b738

6 files changed

Lines changed: 48 additions & 6 deletions

File tree

β€Žlhc_web/lib/core/lhcore/lhfileupload.phpβ€Ž

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ protected function handle_file_upload($uploaded_file, $name, $size, $type, $erro
132132
if (function_exists('finfo_open')) {
133133
$finfo = finfo_open(FILEINFO_MIME_TYPE);
134134
$actualMime = finfo_file($finfo, $fileUpload->file_path_server);
135-
finfo_close($finfo);
136135
if ($actualMime !== false) {
137136
$declaredMime = $fileUpload->type;
138137
$mimeAliases = ['text/xml' => 'application/xml','application/x-rar' => 'application/x-rar-compressed', 'application/x-compressed' => 'application/x-rar-compressed', 'application/x-zip-compressed' => 'application/zip', 'image/jpg' => 'image/jpeg', 'image/pjpeg' => 'image/jpeg', 'image/x-png' => 'image/png'];

β€Žlhc_web/lib/core/lhcore/lhfileuploadadmin.phpβ€Ž

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ protected function handle_file_upload($uploaded_file, $name, $size, $type, $erro
8989
if (function_exists('finfo_open')) {
9090
$finfo = finfo_open(FILEINFO_MIME_TYPE);
9191
$actualMime = finfo_file($finfo, $fileUpload->file_path_server);
92-
finfo_close($finfo);
9392
if ($actualMime !== false) {
9493
$declaredMime = $fileUpload->type;
9594
$mimeAliases = ['text/xml' => 'application/xml','application/x-rar' => 'application/x-rar-compressed', 'application/x-compressed' => 'application/x-rar-compressed', 'application/x-zip-compressed' => 'application/zip', 'image/jpg' => 'image/jpeg', 'image/pjpeg' => 'image/jpeg', 'image/x-png' => 'image/png'];

β€Žlhc_web/lib/core/lhmailconv/html_parsers/rcube_mime.phpβ€Ž

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,6 @@ public static function file_content_type($path, $name, $failover = 'application/
742742
if ($finfo) {
743743
$func = $is_stream ? 'finfo_buffer' : 'finfo_file';
744744
$mime_type = $func($finfo, $path, FILEINFO_MIME_TYPE);
745-
finfo_close($finfo);
746745
}
747746
}
748747

β€Žlhc_web/lib/core/lhtheme/lhthemevalidator.phpβ€Ž

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ public static function get_mime($file) {
146146
if (function_exists("finfo_file")) {
147147
$finfo = finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
148148
$mime = finfo_file($finfo, $file);
149-
finfo_close($finfo);
150149
return $mime;
151150
} else if (function_exists("mime_content_type")) {
152151
return mime_content_type($file);

β€Žlhc_web/modules/lhabstract/downloadbinnary.phpβ€Ž

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
$finfo = finfo_open(FILEINFO_MIME_TYPE);
66
$mimeType = finfo_file($finfo, $ObjectData->file_path_server) . "\n";
7-
finfo_close($finfo);
87

98
header('Content-type: '.$mimeType);
109
header('Content-Disposition: attachment; filename="'.$ObjectData->id.'_file_'.$ObjectData->extension.'"');

β€Žlhc_web/modules/lhfile/downloadfile.phpβ€Ž

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,56 @@
33
try {
44

55
if (isset($_GET['modal']) && $_GET['modal'] == 'external') {
6+
7+
// External image zoom is only used by the operator chat interface. Requiring a
8+
// logged in user prevents this anonymous view from being abused to reflect an
9+
// attacker supplied URL into an <img src>, which could otherwise force victims'
10+
// browsers to request arbitrary/internal hosts (client side SSRF).
11+
if (!erLhcoreClassUser::instance()->isLogged()) {
12+
http_response_code(403);
13+
exit;
14+
}
15+
16+
// Only http/https images are allowed. This drops gopher/ftp/mailto/... and any
17+
// other scheme that erLhcoreClassBBCode::esc_url() would otherwise let through.
18+
$src = erLhcoreClassChatValidator::sanitizeUrl(isset($_GET['src']) ? $_GET['src'] : '');
19+
20+
if ($src === '') {
21+
http_response_code(400);
22+
exit;
23+
}
24+
25+
// Only allow URLs pointing to an external (public) host. Resolve the host and
26+
// reject loopback/private/reserved destinations so this view cannot be used to
27+
// make a visitor's browser probe internal services (e.g. 169.254.169.254).
28+
$srcHost = parse_url($src, PHP_URL_HOST);
29+
30+
if ($srcHost === null || $srcHost === false || $srcHost === '' || strtolower($srcHost) === 'localhost') {
31+
http_response_code(403);
32+
exit;
33+
}
34+
35+
// Literal IPs are validated as-is; hostnames are resolved first
36+
if (filter_var($srcHost, FILTER_VALIDATE_IP) !== false) {
37+
$resolvedIp = $srcHost;
38+
} else {
39+
$resolvedIp = gethostbyname($srcHost);
40+
41+
if ($resolvedIp === $srcHost || filter_var($resolvedIp, FILTER_VALIDATE_IP) === false) {
42+
// Host could not be resolved, do not reflect it
43+
http_response_code(403);
44+
exit;
45+
}
46+
}
47+
48+
if (filter_var($resolvedIp, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
49+
http_response_code(403);
50+
exit;
51+
}
52+
653
$tpl = erLhcoreClassTemplate::getInstance('lhchat/zoomimage.tpl.php');
754
$tpl->set('externalImage', true);
8-
$tpl->set('fileImage', $_GET['src']);
55+
$tpl->set('fileImage', $src);
956
echo $tpl->fetch();
1057
exit;
1158
}

0 commit comments

Comments
Β (0)