mirror of
https://github.com/Studio-42/elFinder.git
synced 2026-06-19 07:37:19 +00:00
11b5e6b82d
- refresh CSRF TTL after successful open/info/protected commands - extend token lifetime on open requests during normal navigation - limit CSRF protection to stable write commands - fix callback script syntax in OAuth popup handling
642 lines
19 KiB
PHP
642 lines
19 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Default elFinder connector
|
|
*
|
|
* @author Dmitry (dio) Levashov
|
|
**/
|
|
class elFinderConnector
|
|
{
|
|
/**
|
|
* elFinder instance
|
|
*
|
|
* @var elFinder
|
|
**/
|
|
protected $elFinder;
|
|
|
|
/**
|
|
* Options
|
|
*
|
|
* @var array
|
|
**/
|
|
protected $options = array();
|
|
|
|
/**
|
|
* Must be use output($data) $data['header']
|
|
*
|
|
* @var string
|
|
* @deprecated
|
|
**/
|
|
protected $header = '';
|
|
|
|
/**
|
|
* HTTP request method
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $reqMethod = '';
|
|
|
|
/**
|
|
* Content type of output JSON
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $contentType = 'Content-Type: application/json; charset=utf-8';
|
|
|
|
/**
|
|
* CSRF token header name
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $csrfHeaderName = 'X-elFinder-CSRF';
|
|
|
|
/**
|
|
* JSON response key for CSRF token
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $csrfResponseKey = 'csrf';
|
|
|
|
/**
|
|
* Session key for CSRF token data
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $csrfSessionKey = 'elfinder.csrf';
|
|
|
|
/**
|
|
* Default CSRF token TTL seconds
|
|
*
|
|
* @var int
|
|
*/
|
|
protected static $csrfTokenTtl = 900;
|
|
|
|
/**
|
|
* Commands that require CSRF header validation
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $csrfProtectedCmds = array(
|
|
'archive' => true,
|
|
'chmod' => true,
|
|
'duplicate' => true,
|
|
'extract' => true,
|
|
'mkdir' => true,
|
|
'mkfile' => true,
|
|
'paste' => true,
|
|
'put' => true,
|
|
'rename' => true,
|
|
'resize' => true,
|
|
'rm' => true,
|
|
'upload' => true
|
|
);
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param $elFinder
|
|
* @param bool $debug
|
|
*
|
|
* @author Dmitry (dio) Levashov
|
|
*/
|
|
public function __construct($elFinder, $debug = false)
|
|
{
|
|
|
|
$this->elFinder = $elFinder;
|
|
$this->reqMethod = strtoupper($_SERVER["REQUEST_METHOD"]);
|
|
if ($debug) {
|
|
self::$contentType = 'Content-Type: text/plain; charset=utf-8';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine whether the command requires CSRF validation
|
|
*
|
|
* @param string $cmd
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function csrfProtectedCommand($cmd)
|
|
{
|
|
return isset(self::$csrfProtectedCmds[$cmd]);
|
|
}
|
|
|
|
/**
|
|
* Determine whether current request should issue CSRF token
|
|
*
|
|
* @param string $cmd
|
|
* @param array $src
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function shouldIssueCsrfToken($cmd, array $src)
|
|
{
|
|
return ($cmd === 'open' && !empty($src['init']));
|
|
}
|
|
|
|
/**
|
|
* Determine whether current request should refresh CSRF token TTL
|
|
*
|
|
* @param string $cmd
|
|
* @param array $src
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function shouldRefreshCsrfToken($cmd, array $src)
|
|
{
|
|
return (
|
|
($cmd === 'info' && !empty($src['reload']))
|
|
|| $cmd === 'open'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate or reuse current CSRF token
|
|
*
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
protected function issueCsrfToken()
|
|
{
|
|
$session = $this->elFinder->getSession();
|
|
$now = time();
|
|
$tokenData = $session->get(self::$csrfSessionKey, array());
|
|
|
|
if (!is_array($tokenData)) {
|
|
$tokenData = array();
|
|
}
|
|
|
|
if (empty($tokenData['token']) || empty($tokenData['expires']) || (int)$tokenData['expires'] <= $now) {
|
|
$tokenData = array(
|
|
'token' => $this->generateCsrfToken(),
|
|
'expires' => $now + self::$csrfTokenTtl
|
|
);
|
|
$session->set(self::$csrfSessionKey, $tokenData);
|
|
}
|
|
|
|
return $tokenData['token'];
|
|
}
|
|
|
|
/**
|
|
* Generate a random CSRF token string with old PHP compatibility
|
|
*
|
|
* @return string
|
|
* @throws Exception
|
|
*/
|
|
protected function generateCsrfToken()
|
|
{
|
|
if (function_exists('random_bytes')) {
|
|
return bin2hex(random_bytes(32));
|
|
}
|
|
|
|
if (function_exists('openssl_random_pseudo_bytes')) {
|
|
return bin2hex(openssl_random_pseudo_bytes(32));
|
|
}
|
|
|
|
return sha1(uniqid(mt_rand(), true) . microtime(true));
|
|
}
|
|
|
|
/**
|
|
* Get HTTP request header value
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function getRequestHeader($name)
|
|
{
|
|
$serverKey = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
|
|
if (isset($_SERVER[$serverKey])) {
|
|
return trim($_SERVER[$serverKey]);
|
|
} else {
|
|
if (function_exists('getallheaders')) {
|
|
$headers = getallheaders();
|
|
if (isset($headers[$name])) {
|
|
return trim($headers[$name]);
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate CSRF token header for protected commands
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function validateCsrfToken()
|
|
{
|
|
$session = $this->elFinder->getSession();
|
|
$tokenData = $session->get(self::$csrfSessionKey, array());
|
|
$headerToken = $this->getRequestHeader(self::$csrfHeaderName);
|
|
$now = time();
|
|
|
|
if (!is_array($tokenData) || empty($tokenData['token']) || empty($tokenData['expires'])) {
|
|
return false;
|
|
}
|
|
|
|
if ((int)$tokenData['expires'] <= $now) {
|
|
$session->remove(self::$csrfSessionKey);
|
|
return false;
|
|
}
|
|
|
|
if ($headerToken === '') {
|
|
return false;
|
|
}
|
|
|
|
if (function_exists('hash_equals')) {
|
|
return hash_equals($tokenData['token'], $headerToken);
|
|
}
|
|
|
|
return ($tokenData['token'] === $headerToken);
|
|
}
|
|
|
|
/**
|
|
* Refresh current CSRF token TTL if header matches the active token
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function refreshCsrfTokenTtl()
|
|
{
|
|
$session = $this->elFinder->getSession();
|
|
$tokenData = $session->get(self::$csrfSessionKey, array());
|
|
$headerToken = $this->getRequestHeader(self::$csrfHeaderName);
|
|
$now = time();
|
|
$isValid = false;
|
|
|
|
if (!is_array($tokenData) || empty($tokenData['token']) || empty($tokenData['expires']) || $headerToken === '') {
|
|
return false;
|
|
}
|
|
|
|
if ((int)$tokenData['expires'] <= $now) {
|
|
$session->remove(self::$csrfSessionKey);
|
|
return false;
|
|
}
|
|
|
|
if (function_exists('hash_equals')) {
|
|
$isValid = hash_equals($tokenData['token'], $headerToken);
|
|
} else {
|
|
$isValid = ($tokenData['token'] === $headerToken);
|
|
}
|
|
|
|
if (!$isValid) {
|
|
return false;
|
|
}
|
|
|
|
$tokenData['expires'] = $now + self::$csrfTokenTtl;
|
|
$session->set(self::$csrfSessionKey, $tokenData);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Output CSRF validation error response
|
|
*
|
|
* @return void
|
|
* @throws elFinderAbortException
|
|
*/
|
|
protected function outputCsrfError()
|
|
{
|
|
$this->output(array(
|
|
'error' => $this->elFinder->error(elFinder::ERROR_PERM_DENIED, 'Invalid request. Please reload.'),
|
|
'csrfReload' => true,
|
|
'header' => array(
|
|
'HTTP/1.1 403 Forbidden',
|
|
self::$contentType
|
|
)
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Execute elFinder command and output result
|
|
*
|
|
* @return void
|
|
* @throws Exception
|
|
* @author Dmitry (dio) Levashov
|
|
*/
|
|
public function run()
|
|
{
|
|
$isPost = $this->reqMethod === 'POST';
|
|
$src = $isPost ? array_merge($_GET, $_POST) : $_GET;
|
|
$maxInputVars = (!$src || isset($src['targets'])) ? ini_get('max_input_vars') : null;
|
|
if ((!$src || $maxInputVars) && $rawPostData = file_get_contents('php://input')) {
|
|
// for max_input_vars and supports IE XDomainRequest()
|
|
$parts = explode('&', $rawPostData);
|
|
if (!$src || $maxInputVars < count($parts)) {
|
|
$src = array();
|
|
foreach ($parts as $part) {
|
|
list($key, $value) = array_pad(explode('=', $part), 2, '');
|
|
$key = rawurldecode($key);
|
|
if (preg_match('/^(.+?)\[([^\[\]]*)\]$/', $key, $m)) {
|
|
$key = $m[1];
|
|
$idx = $m[2];
|
|
if (!isset($src[$key])) {
|
|
$src[$key] = array();
|
|
}
|
|
if ($idx) {
|
|
$src[$key][$idx] = rawurldecode($value);
|
|
} else {
|
|
$src[$key][] = rawurldecode($value);
|
|
}
|
|
} else {
|
|
$src[$key] = rawurldecode($value);
|
|
}
|
|
}
|
|
$_POST = $this->input_filter($src);
|
|
$_REQUEST = $this->input_filter(array_merge_recursive($src, $_REQUEST));
|
|
}
|
|
}
|
|
|
|
if (isset($src['targets']) && $this->elFinder->maxTargets && count($src['targets']) > $this->elFinder->maxTargets) {
|
|
$this->output(array('error' => $this->elFinder->error(elFinder::ERROR_MAX_TARGTES)));
|
|
}
|
|
|
|
$cmd = isset($src['cmd']) ? $src['cmd'] : '';
|
|
$args = array();
|
|
|
|
if (!function_exists('json_encode')) {
|
|
$error = $this->elFinder->error(elFinder::ERROR_CONF, elFinder::ERROR_CONF_NO_JSON);
|
|
$this->output(array('error' => '{"error":["' . implode('","', $error) . '"]}', 'raw' => true));
|
|
}
|
|
|
|
if (!$this->elFinder->loaded()) {
|
|
$this->output(array('error' => $this->elFinder->error(elFinder::ERROR_CONF, elFinder::ERROR_CONF_NO_VOL), 'debug' => $this->elFinder->mountErrors));
|
|
}
|
|
|
|
// telepat_mode: on
|
|
if (!$cmd && $isPost) {
|
|
$this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UPLOAD, elFinder::ERROR_UPLOAD_TOTAL_SIZE), 'header' => 'Content-Type: text/html'));
|
|
}
|
|
// telepat_mode: off
|
|
|
|
if (!$this->elFinder->commandExists($cmd)) {
|
|
$this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UNKNOWN_CMD)));
|
|
}
|
|
|
|
if ($this->csrfProtectedCommand($cmd)) {
|
|
if (!$this->validateCsrfToken()) {
|
|
$this->outputCsrfError();
|
|
}
|
|
$this->refreshCsrfTokenTtl();
|
|
}
|
|
|
|
// collect required arguments to exec command
|
|
$hasFiles = false;
|
|
foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) {
|
|
if ($name === 'FILES') {
|
|
if (isset($_FILES)) {
|
|
$hasFiles = true;
|
|
} elseif ($req) {
|
|
$this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd)));
|
|
}
|
|
} else {
|
|
$arg = isset($src[$name]) ? $src[$name] : '';
|
|
|
|
if (!is_array($arg) && $req !== '') {
|
|
$arg = trim($arg);
|
|
}
|
|
if ($req && $arg === '') {
|
|
$this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd)));
|
|
}
|
|
$args[$name] = $arg;
|
|
}
|
|
}
|
|
|
|
$args['debug'] = isset($src['debug']) ? !!$src['debug'] : false;
|
|
|
|
$args = $this->input_filter($args);
|
|
if ($hasFiles) {
|
|
$args['FILES'] = $_FILES;
|
|
}
|
|
|
|
try {
|
|
$result = $this->elFinder->exec($cmd, $args);
|
|
if (is_array($result) && !isset($result['error'])) {
|
|
if ($this->shouldIssueCsrfToken($cmd, $src)) {
|
|
$result[self::$csrfResponseKey] = $this->issueCsrfToken();
|
|
} else if ($this->shouldRefreshCsrfToken($cmd, $src)) {
|
|
$this->refreshCsrfTokenTtl();
|
|
}
|
|
}
|
|
$this->output($result);
|
|
} catch (elFinderAbortException $e) {
|
|
// connection aborted
|
|
// unlock session data for multiple access
|
|
$this->elFinder->getSession()->close();
|
|
// HTTP response code
|
|
header('HTTP/1.0 204 No Content');
|
|
// clear output buffer
|
|
while (ob_get_level() && ob_end_clean()) {
|
|
}
|
|
exit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the header.
|
|
*
|
|
* @param array|string $value HTTP header(s)
|
|
*/
|
|
public function setHeader($value)
|
|
{
|
|
$this->header = $value;
|
|
}
|
|
|
|
/**
|
|
* Output json
|
|
*
|
|
* @param array data to output
|
|
*
|
|
* @return void
|
|
* @throws elFinderAbortException
|
|
* @author Dmitry (dio) Levashov
|
|
*/
|
|
protected function output(array $data)
|
|
{
|
|
// unlock session data for multiple access
|
|
$this->elFinder->getSession()->close();
|
|
// client disconnect should abort
|
|
ignore_user_abort(false);
|
|
|
|
if ($this->header) {
|
|
self::sendHeader($this->header);
|
|
}
|
|
|
|
if (isset($data['pointer'])) {
|
|
// set time limit to 0
|
|
elFinder::extendTimeLimit(0);
|
|
|
|
// send optional header
|
|
if (!empty($data['header'])) {
|
|
self::sendHeader($data['header']);
|
|
}
|
|
|
|
// clear output buffer
|
|
while (ob_get_level() && ob_end_clean()) {
|
|
}
|
|
|
|
$toEnd = true;
|
|
$fp = $data['pointer'];
|
|
$sendData = !($this->reqMethod === 'HEAD' || !empty($data['info']['xsendfile']));
|
|
$psize = null;
|
|
if (($this->reqMethod === 'GET' || !$sendData)
|
|
&& (elFinder::isSeekableStream($fp) || elFinder::isSeekableUrl($fp))
|
|
&& (array_search('Accept-Ranges: none', headers_list()) === false)) {
|
|
header('Accept-Ranges: bytes');
|
|
if (!empty($_SERVER['HTTP_RANGE'])) {
|
|
$size = $data['info']['size'];
|
|
$end = $size - 1;
|
|
if (preg_match('/bytes=(\d*)-(\d*)(,?)/i', $_SERVER['HTTP_RANGE'], $matches)) {
|
|
if (empty($matches[3])) {
|
|
if (empty($matches[1]) && $matches[1] !== '0') {
|
|
$start = $size - $matches[2];
|
|
} else {
|
|
$start = intval($matches[1]);
|
|
if (!empty($matches[2])) {
|
|
$end = intval($matches[2]);
|
|
if ($end >= $size) {
|
|
$end = $size - 1;
|
|
}
|
|
$toEnd = ($end == ($size - 1));
|
|
}
|
|
}
|
|
$psize = $end - $start + 1;
|
|
|
|
header('HTTP/1.1 206 Partial Content');
|
|
header('Content-Length: ' . $psize);
|
|
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
|
|
|
|
// Apache mod_xsendfile dose not support range request
|
|
if (isset($data['info']['xsendfile']) && strtolower($data['info']['xsendfile']) === 'x-sendfile') {
|
|
if (function_exists('header_remove')) {
|
|
header_remove($data['info']['xsendfile']);
|
|
} else {
|
|
header($data['info']['xsendfile'] . ':');
|
|
}
|
|
unset($data['info']['xsendfile']);
|
|
if ($this->reqMethod !== 'HEAD') {
|
|
$sendData = true;
|
|
}
|
|
}
|
|
|
|
$sendData && !elFinder::isSeekableUrl($fp) && fseek($fp, $start);
|
|
}
|
|
}
|
|
}
|
|
if ($sendData && is_null($psize)) {
|
|
elFinder::rewind($fp);
|
|
}
|
|
} else {
|
|
header('Accept-Ranges: none');
|
|
if (isset($data['info']) && !$data['info']['size']) {
|
|
if (function_exists('header_remove')) {
|
|
header_remove('Content-Length');
|
|
} else {
|
|
header('Content-Length:');
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($sendData) {
|
|
if ($toEnd || elFinder::isSeekableUrl($fp)) {
|
|
// PHP < 5.6 has a bug of fpassthru
|
|
// see https://bugs.php.net/bug.php?id=66736
|
|
if (version_compare(PHP_VERSION, '5.6', '<')) {
|
|
file_put_contents('php://output', $fp);
|
|
} else {
|
|
fpassthru($fp);
|
|
}
|
|
} else {
|
|
$out = fopen('php://output', 'wb');
|
|
stream_copy_to_stream($fp, $out, $psize);
|
|
fclose($out);
|
|
}
|
|
}
|
|
|
|
if (!empty($data['volume'])) {
|
|
$data['volume']->close($fp, $data['info']['hash']);
|
|
} else {
|
|
fclose($fp);
|
|
}
|
|
exit();
|
|
} else {
|
|
self::outputJson($data);
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove null & stripslashes applies on "magic_quotes_gpc"
|
|
*
|
|
* @param mixed $args
|
|
*
|
|
* @return mixed
|
|
* @author Naoki Sawada
|
|
*/
|
|
protected function input_filter($args)
|
|
{
|
|
static $magic_quotes_gpc = NULL;
|
|
|
|
if ($magic_quotes_gpc === NULL)
|
|
$magic_quotes_gpc = (version_compare(PHP_VERSION, '5.4', '<') && get_magic_quotes_gpc());
|
|
|
|
if (is_array($args)) {
|
|
return array_map(array(& $this, 'input_filter'), $args);
|
|
}
|
|
$res = str_replace("\0", '', $args);
|
|
$magic_quotes_gpc && ($res = stripslashes($res));
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Send HTTP header
|
|
*
|
|
* @param string|array $header optional header
|
|
*/
|
|
protected static function sendHeader($header = null)
|
|
{
|
|
if ($header) {
|
|
if (is_array($header)) {
|
|
foreach ($header as $h) {
|
|
header($h);
|
|
}
|
|
} else {
|
|
header($header);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Output JSON
|
|
*
|
|
* @param array $data
|
|
*/
|
|
public static function outputJson($data)
|
|
{
|
|
// send header
|
|
$header = isset($data['header']) ? $data['header'] : self::$contentType;
|
|
self::sendHeader($header);
|
|
|
|
unset($data['header']);
|
|
|
|
if (!empty($data['raw']) && isset($data['error'])) {
|
|
$out = $data['error'];
|
|
} else {
|
|
if (isset($data['debug']) && isset($data['debug']['backendErrors'])) {
|
|
$data['debug']['backendErrors'] = array_merge($data['debug']['backendErrors'], elFinder::$phpErrors);
|
|
}
|
|
$out = json_encode($data);
|
|
}
|
|
|
|
// clear output buffer
|
|
while (ob_get_level() && ob_end_clean()) {
|
|
}
|
|
|
|
header('Content-Length: ' . strlen($out));
|
|
|
|
echo $out;
|
|
|
|
flush();
|
|
}
|
|
}// END class
|