Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 179 additions & 13 deletions tinyfilemanager.php
Original file line number Diff line number Diff line change
Expand Up @@ -604,17 +604,77 @@ function get_file_path()

$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];
// 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();
}

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");
// 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();
Expand All @@ -639,7 +699,18 @@ function get_file_path()
@$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) {
Expand All @@ -650,7 +721,24 @@ function get_file_path()
$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();
Expand Down Expand Up @@ -734,9 +822,16 @@ function get_file_path()
}

// 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 == '') {
Expand All @@ -753,7 +848,7 @@ function get_file_path()
}
$dest .= '/' . basename($from);
// move?
$move = isset($_GET['move']);
$move = isset($_POST['move']);
$move = fm_clean_path(urldecode($move));
// copy/move/duplicate
if ($from != $dest) {
Expand Down Expand Up @@ -1540,9 +1635,28 @@ function getUploadExt()
<strong>Destination folder:</strong> <?php echo fm_enc(fm_convert_win(FM_ROOT_PATH . '/' . FM_PATH)) ?>
</p>
<p>
<b><a href="?p=<?php echo urlencode(FM_PATH) ?>&amp;copy=<?php echo urlencode($copy) ?>&amp;finish=1"><i class="fa fa-check-circle"></i> Copy</a></b> &nbsp;
<b><a href="?p=<?php echo urlencode(FM_PATH) ?>&amp;copy=<?php echo urlencode($copy) ?>&amp;finish=1&amp;move=1"><i class="fa fa-check-circle"></i> Move</a></b> &nbsp;
<b><a href="?p=<?php echo urlencode(FM_PATH) ?>" class="text-danger"><i class="fa fa-times-circle"></i> Cancel</a></b>
<form method="post" action="" style="display:inline;">
<input type="hidden" name="p" value="<?php echo fm_enc(FM_PATH) ?>">
<input type="hidden" name="copy" value="<?php echo fm_enc($copy) ?>">
<input type="hidden" name="finish" value="1">
<input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>">
<button type="submit" class="btn btn-sm btn-success" style="padding: 3px 6px; font-size: 14px;">
<i class="fa fa-check-circle"></i> <?php echo lng('Copy') ?>
</button>
</form>
&nbsp;
<form method="post" action="" style="display:inline;">
<input type="hidden" name="p" value="<?php echo fm_enc(FM_PATH) ?>">
<input type="hidden" name="copy" value="<?php echo fm_enc($copy) ?>">
<input type="hidden" name="finish" value="1">
<input type="hidden" name="move" value="1">
<input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>">
<button type="submit" class="btn btn-sm btn-success" style="padding: 3px 6px; font-size: 14px;">
<i class="fa fa-check-circle"></i> <?php echo lng('Move') ?>
</button>
</form>
&nbsp;
<b><a href="?p=<?php echo urlencode(FM_PATH) ?>" class="text-danger"><i class="fa fa-times-circle"></i> <?php echo lng('Cancel') ?></a></b>
</p>
<p><i><?php echo lng('Select folder') ?></i></p>
<ul class="folders break-word">
Expand Down Expand Up @@ -2357,6 +2471,58 @@ class="edit-file"><i class="fa fa-pencil-square"></i> <?php echo lng('AdvancedEd

// 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.
Expand Down