From b624a2edcaf270e48d10b791b7286b000b6c7daa Mon Sep 17 00:00:00 2001
From: Maalfer
Date: Wed, 7 Jan 2026 15:05:41 +0100
Subject: [PATCH] CSRF and SSRF vulnerabilities fixed
---
tinyfilemanager.php | 194 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 180 insertions(+), 14 deletions(-)
diff --git a/tinyfilemanager.php b/tinyfilemanager.php
index 642f073..5c7d4b7 100644
--- a/tinyfilemanager.php
+++ b/tinyfilemanager.php
@@ -604,17 +604,77 @@ if ((isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_
$url = !empty($_REQUEST["uploadurl"]) && preg_match("|^http(s)?://.+$|", stripslashes($_REQUEST["uploadurl"])) ? stripslashes($_REQUEST["uploadurl"]) : null;
- //prevent 127.* domain and known ports
- $domain = parse_url($url, PHP_URL_HOST);
- $port = parse_url($url, PHP_URL_PORT);
- $knownPorts = [22, 23, 25, 3306];
-
- if (preg_match("/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/i", $domain) || in_array($port, $knownPorts)) {
- $err = array("message" => "URL is not allowed");
+ // Validate URL exists
+ if (!$url) {
+ $err = array("message" => "Invalid URL");
event_callback(array("fail" => $err));
exit();
}
+ // Parse URL components
+ $parsed_url = parse_url($url);
+ if (!$parsed_url || !isset($parsed_url['host'])) {
+ $err = array("message" => "Invalid URL format");
+ event_callback(array("fail" => $err));
+ exit();
+ }
+
+ $host = $parsed_url['host'];
+ $port = isset($parsed_url['port']) ? $parsed_url['port'] : null;
+ $scheme = isset($parsed_url['scheme']) ? strtolower($parsed_url['scheme']) : '';
+
+ // Only allow HTTP and HTTPS protocols
+ if (!in_array($scheme, ['http', 'https'])) {
+ $err = array("message" => "Only HTTP and HTTPS protocols are allowed");
+ event_callback(array("fail" => $err));
+ exit();
+ }
+
+ // Block dangerous ports (expanded list)
+ $blocked_ports = [21, 22, 23, 25, 110, 143, 445, 3306, 3389, 5432, 5984, 6379, 7001, 8020, 8888, 9200, 11211, 27017];
+ if ($port && in_array($port, $blocked_ports)) {
+ $err = array("message" => "Access to this port is not allowed");
+ event_callback(array("fail" => $err));
+ exit();
+ }
+
+ // Resolve hostname to IP addresses
+ $ip_list = @gethostbynamel($host);
+ if ($ip_list === false || empty($ip_list)) {
+ // If DNS resolution fails, check if host is already an IP
+ $resolved_ip = @gethostbyname($host);
+ if ($resolved_ip === $host) {
+ // Check if it's a valid IP address
+ if (filter_var($host, FILTER_VALIDATE_IP)) {
+ $ip_list = [$host];
+ } else {
+ $err = array("message" => "Cannot resolve hostname");
+ event_callback(array("fail" => $err));
+ exit();
+ }
+ } else {
+ $ip_list = [$resolved_ip];
+ }
+ }
+
+ // Validate all resolved IPs are not private/internal
+ foreach ($ip_list as $ip) {
+ if (fm_is_ip_restricted($ip)) {
+ $err = array("message" => "Access to private/internal resources is not allowed");
+ event_callback(array("fail" => $err));
+ exit();
+ }
+ }
+
+ // If host is an IP address, validate it directly as well
+ if (filter_var($host, FILTER_VALIDATE_IP)) {
+ if (fm_is_ip_restricted($host)) {
+ $err = array("message" => "Access to private/internal resources is not allowed");
+ event_callback(array("fail" => $err));
+ exit();
+ }
+ }
+
$use_curl = false;
$temp_file = tempnam(sys_get_temp_dir(), "upload-");
$fileinfo = new stdClass();
@@ -639,7 +699,18 @@ if ((isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_
@$ch = curl_init($url);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 10);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_FILE, $fp);
+ curl_setopt($ch, CURLOPT_USERAGENT, 'TinyFileManager/2.6');
+ // Restrict protocols to HTTP/HTTPS only
+ if (defined('CURLOPT_PROTOCOLS')) {
+ curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
+ }
+ if (defined('CURLOPT_REDIR_PROTOCOLS')) {
+ curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
+ }
@$success = curl_exec($ch);
$curl_info = curl_getinfo($ch);
if (!$success) {
@@ -650,7 +721,24 @@ if ((isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_
$fileinfo->size = $curl_info["size_download"];
$fileinfo->type = $curl_info["content_type"];
} else {
- $ctx = stream_context_create();
+ // Create stream context with timeout and security options
+ $context_options = array(
+ 'http' => array(
+ 'timeout' => 10,
+ 'follow_location' => 1,
+ 'max_redirects' => 3,
+ 'user_agent' => 'TinyFileManager/2.6',
+ 'ignore_errors' => false
+ ),
+ 'https' => array(
+ 'timeout' => 10,
+ 'follow_location' => 1,
+ 'max_redirects' => 3,
+ 'user_agent' => 'TinyFileManager/2.6',
+ 'ignore_errors' => false
+ )
+ );
+ $ctx = stream_context_create($context_options);
@$success = copy($url, $temp_file, $ctx);
if (!$success) {
$err = error_get_last();
@@ -734,9 +822,16 @@ if (isset($_POST['newfilename'], $_POST['newfile'], $_POST['token']) && !FM_READ
}
// Copy folder / file
-if (isset($_GET['copy'], $_GET['finish']) && !FM_READONLY) {
+if (isset($_POST['copy'], $_POST['finish'], $_POST['token']) && !FM_READONLY) {
+ // Validate CSRF token
+ if (!verifyToken($_POST['token'])) {
+ fm_set_msg(lng('Invalid Token.'), 'error');
+ $FM_PATH = FM_PATH;
+ fm_redirect(FM_SELF_URL . '?p=' . urlencode($FM_PATH));
+ }
+
// from
- $copy = urldecode($_GET['copy']);
+ $copy = urldecode($_POST['copy']);
$copy = fm_clean_path($copy);
// empty path
if ($copy == '') {
@@ -753,7 +848,7 @@ if (isset($_GET['copy'], $_GET['finish']) && !FM_READONLY) {
}
$dest .= '/' . basename($from);
// move?
- $move = isset($_GET['move']);
+ $move = isset($_POST['move']);
$move = fm_clean_path(urldecode($move));
// copy/move/duplicate
if ($from != $dest) {
@@ -1540,9 +1635,28 @@ if (isset($_GET['copy']) && !isset($_GET['finish']) && !FM_READONLY) {
Destination folder:
- Copy
- Move
- Cancel
+
+
+
+
+
@@ -2357,6 +2471,58 @@ fm_show_footer();
// Functions
+/**
+ * Check if an IP address is private, loopback, or otherwise restricted
+ * Prevents SSRF attacks by blocking access to internal resources
+ * @param string $ip The IP address to check
+ * @return bool True if IP is restricted, false if safe to access
+ */
+function fm_is_ip_restricted($ip)
+{
+ // Remove brackets from IPv6 addresses
+ $ip = trim($ip, '[]');
+
+ // Validate IP format
+ if (!filter_var($ip, FILTER_VALIDATE_IP)) {
+ return true; // Invalid IP is restricted
+ }
+
+ // Check for private or reserved IP ranges using PHP filters
+ // This covers: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8
+ // IPv6: fd00::/8, fe80::/10, ::1/128, and other reserved ranges
+ if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+ return true; // Private or reserved IP
+ }
+
+ // Additional IPv6 loopback check for variations
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+ $ipv6_lower = strtolower($ip);
+ // Check for ::1 and its expanded forms (0:0:0:0:0:0:0:1, etc.)
+ if (preg_match('/^(0*:){1,7}0*1$/', $ipv6_lower) || $ipv6_lower === '::1') {
+ return true;
+ }
+ }
+
+ // Additional IPv4 checks for edge cases
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+ // Block 0.0.0.0/8 (including shorthand like "0")
+ $parts = explode('.', $ip);
+ if (count($parts) === 4 && $parts[0] === '0') {
+ return true;
+ }
+ // Also block single "0" or "0.0.0.0"
+ if ($ip === '0' || $ip === '0.0.0.0') {
+ return true;
+ }
+ // Block link-local 169.254.0.0/16
+ if (preg_match('/^169\.254\./', $ip)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
/**
* It prints the css/js files into html
* @param key The key of the external file to print.