Fix File Manager: file deletion, special chars, upload auth (Root FM)

- Fix delete for domain and Root File Manager: use sudo helper when
  lscpd/executioner fails (TOKEN/sendCommand issues)
- Add safe-delete-path and safe-move-path helpers for base64 path handling
- Add ACLManager.isPathInsideHome and isFilePathSafeForShell for path validation
- Fix upload authorization for Root File Manager (domainName empty)
- Harden outputExecutioner result checks to prevent 500 on None
- Update Bootstrap CDN for CSP compatibility
- Improve error display and a11y focus management in modals
- Resolves #1670: files with special characters can be uploaded/deleted
This commit is contained in:
master3395
2026-02-04 00:55:58 +01:00
parent 579af7d691
commit 06d88e6481
12 changed files with 294 additions and 136 deletions

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/png" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'filemanager/images/fonts/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'filemanager/css/fileManager.css' %}">
@@ -52,7 +52,7 @@
</div-->
<ul class="nav mr-10">
<li class="nav-item">
<a onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
<a id="uploadTriggerBtn" onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
</li>
<li class="nav-item">
<a onclick="return false;" ng-click="showCreateFileModal()" class="nav-link point-events" href="#"><i class="fa fa-plus-square" aria-hidden="true"></i> {% trans "New File" %}</a>
@@ -608,7 +608,7 @@
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<!-- HTML Editor Include -->

View File

@@ -1,4 +1,5 @@
import os
import base64
from django.shortcuts import HttpResponse
import json
@@ -160,12 +161,67 @@ class FileManager:
def returnPathEnclosed(self, path):
return "'" + path + "'"
def _moveViaPythonBase64(self, src_path, dest_path, user):
"""Fallback: use helper script or Python to move when mv fails (handles special chars)."""
try:
import subprocess
s_b64 = base64.b64encode(src_path.encode('utf-8')).decode('ascii')
d_b64 = base64.b64encode(dest_path.encode('utf-8')).decode('ascii')
helper = '/usr/local/CyberCP/bin/safe-move-path'
if os.path.isfile(helper) and os.access(helper, os.X_OK):
cmd = [helper, s_b64, d_b64]
if os.getuid() != 0:
cmd = ['sudo', '-n'] + cmd
try:
res = subprocess.run(cmd, capture_output=True, timeout=30)
if res.returncode == 0:
return True
except Exception as e:
logging.writeToFile(f"_moveViaPythonBase64 sudo helper failed: {str(e)}")
command = '%s %s %s' % (helper, s_b64, d_b64)
else:
code = "import shutil,base64,sys; s=base64.b64decode(sys.argv[1]).decode(); d=base64.b64decode(sys.argv[2]).decode(); shutil.move(s,d)"
command = "/usr/bin/python3 -c '%s' %s %s" % (code, s_b64, d_b64)
result = ProcessUtilities.executioner(command, user)
return result == 1
except Exception as e:
logging.writeToFile(f"_moveViaPythonBase64 failed: {str(e)}")
return False
def _deleteViaPythonBase64(self, path, user):
"""Fallback: use helper script or Python to delete when rm fails (handles special chars)."""
try:
import subprocess
p_b64 = base64.b64encode(path.encode('utf-8')).decode('ascii')
helper = '/usr/local/CyberCP/bin/safe-delete-path'
if os.path.isfile(helper) and os.access(helper, os.X_OK):
cmd = [helper, p_b64]
if os.getuid() != 0:
cmd = ['sudo', '-n'] + cmd
try:
res = subprocess.run(cmd, capture_output=True, timeout=30)
if res.returncode == 0:
return True
except Exception as e:
logging.writeToFile(f"_deleteViaPythonBase64 sudo helper failed: {str(e)}")
if os.path.isfile(helper) and os.access(helper, os.X_OK):
command = '%s %s' % (helper, p_b64)
else:
code = "import os,base64,sys,shutil; p=base64.b64decode(sys.argv[1]).decode(); (os.path.isfile(p) and os.remove(p)) or (os.path.isdir(p) and shutil.rmtree(p))"
command = "/usr/bin/python3 -c '%s' %s" % (code, p_b64)
result = ProcessUtilities.executioner(command, user)
return result == 1
except Exception as e:
logging.writeToFile(f"_deleteViaPythonBase64 failed: {str(e)}")
return False
def changeOwner(self, path):
try:
domainName = self.data['domainName']
website = Websites.objects.get(domain=domainName)
homePath = '/home/%s' % (domainName)
if path.find('..') > -1:
if not ACLManager.isPathInsideHome(path, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = "chown -R " + website.externalApp + ':' + website.externalApp + ' ' + self.returnPathEnclosed(path)
@@ -186,8 +242,7 @@ class FileManager:
pathCheck = '/home/%s' % (domainName)
if self.data['completeStartingPath'].find(pathCheck) == -1 or self.data['completeStartingPath'].find(
'..') > -1:
if not ACLManager.isPathInsideHome(self.data['completeStartingPath'], pathCheck):
return self.ajaxPre(0, 'Not allowed to browse this path, going back home!')
command = "ls -la --group-directories-first " + self.returnPathEnclosed(
@@ -197,8 +252,7 @@ class FileManager:
except:
pathCheck = '/'
if self.data['completeStartingPath'].find(pathCheck) == -1 or self.data['completeStartingPath'].find(
'..') > -1:
if not ACLManager.isPathInsideHome(self.data['completeStartingPath'], pathCheck):
return self.ajaxPre(0, 'Not allowed to browse this path, going back home!')
command = "ls -la --group-directories-first " + self.returnPathEnclosed(
@@ -314,7 +368,7 @@ class FileManager:
website = Websites.objects.get(domain=domainName)
homePath = '/home/%s' % (domainName)
if self.data['fileName'].find('..') > -1 or self.data['fileName'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['fileName'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = "touch " + self.returnPathEnclosed(self.data['fileName'])
@@ -327,7 +381,7 @@ class FileManager:
except:
homePath = '/'
if self.data['fileName'].find('..') > -1 or self.data['fileName'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['fileName'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = "touch " + self.returnPathEnclosed(self.data['fileName'])
@@ -349,7 +403,7 @@ class FileManager:
homePath = '/home/%s' % (domainName)
if self.data['folderName'].find('..') > -1 or self.data['folderName'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['folderName'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = "mkdir " + self.returnPathEnclosed(self.data['folderName'])
@@ -363,7 +417,7 @@ class FileManager:
except:
homePath = '/'
if self.data['folderName'].find('..') > -1 or self.data['folderName'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['folderName'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = "mkdir " + self.returnPathEnclosed(self.data['folderName'])
@@ -392,25 +446,27 @@ class FileManager:
website = Websites.objects.get(domain=domainName)
self.homePath = '/home/%s' % (domainName)
logging.CyberCPLogFileWriter.writeToFile(f"Attempting to delete files/folders for domain: {domainName}")
logging.writeToFile(f"Attempting to delete files/folders for domain: {domainName}")
RemoveOK = 1
# Test if directory is writable
command = 'touch %s/public_html/hello.txt' % (self.homePath)
result = ProcessUtilities.outputExecutioner(command)
if result is None:
result = ''
if result.find('cannot touch') > -1:
if isinstance(result, (str, bytes)) and ('cannot touch' in str(result) or 'Permission denied' in str(result)):
RemoveOK = 0
logging.CyberCPLogFileWriter.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
logging.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
# Remove immutable flag from entire directory
# Remove immutable flag from entire directory (executioner returns 1=success, 0=failure)
command = 'chattr -R -i %s' % (self.homePath)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
if result != 1:
logging.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr -i from {self.homePath}")
logging.writeToFile(f"Successfully removed chattr -i from {self.homePath}")
else:
command = 'rm -f %s/public_html/hello.txt' % (self.homePath)
@@ -421,24 +477,22 @@ class FileManager:
itemPath = self.data['path'] + '/' + item
# Security check - prevent path traversal
if itemPath.find('..') > -1 or itemPath.find(self.homePath) == -1:
logging.CyberCPLogFileWriter.writeToFile(f"Security violation: Attempted to delete outside home directory: {itemPath}")
if not ACLManager.isPathInsideHome(itemPath, self.homePath):
logging.writeToFile(f"Security violation: Attempted to delete outside home directory: {itemPath}")
return self.ajaxPre(0, 'Not allowed to delete files outside home directory!')
logging.CyberCPLogFileWriter.writeToFile(f"Deleting: {itemPath}")
logging.writeToFile(f"Deleting: {itemPath}")
if skipTrash:
# Permanent deletion
# Permanent deletion (executioner returns 1=success, 0=failure)
command = 'rm -rf ' + self.returnPathEnclosed(itemPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to delete {itemPath}: {result}")
# Try with sudo if available
command = 'sudo rm -rf ' + self.returnPathEnclosed(itemPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
return self.ajaxPre(0, f'Failed to delete {item}: {result}')
logging.CyberCPLogFileWriter.writeToFile(f"Successfully deleted: {itemPath}")
if result != 1:
logging.writeToFile(f"Failed to delete {itemPath}: result={result}, trying Python fallback")
# Fallback: Python+base64 to handle special chars in paths
if not self._deleteViaPythonBase64(itemPath, website.externalApp):
return self.ajaxPre(0, f'Failed to delete {item}')
logging.writeToFile(f"Successfully deleted: {itemPath}")
## Update disk usage in background
command = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/IncScheduler.py UpdateDiskUsageForceDomain --domainName %s" % (domainName)
@@ -447,46 +501,44 @@ class FileManager:
# Move to trash
trashPath = '%s/.trash' % (self.homePath)
# Ensure trash directory exists
# Ensure trash directory exists (executioner returns 1=success, 0=failure)
command = 'mkdir -p %s' % (trashPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to create trash directory: {result}")
return self.ajaxPre(0, f'Failed to create trash directory: {result}')
if result != 1:
logging.writeToFile(f"Failed to create trash directory: result={result}")
return self.ajaxPre(0, f'Failed to create trash directory')
# Save to trash database
try:
Trash(website=website, originalPath=self.returnPathEnclosed(self.data['path']),
fileName=self.returnPathEnclosed(item)).save()
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to save trash record: {str(e)}")
logging.writeToFile(f"Failed to save trash record: {str(e)}")
# Move to trash
# Move to trash (executioner returns 1=success, 0=failure)
command = 'mv %s %s' % (self.returnPathEnclosed(itemPath), trashPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to move to trash {itemPath}: {result}")
# Try with sudo if available
command = 'sudo mv %s %s' % (self.returnPathEnclosed(itemPath), trashPath)
result = ProcessUtilities.executioner(command, website.externalApp)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
return self.ajaxPre(0, f'Failed to move {item} to trash: {result}')
logging.CyberCPLogFileWriter.writeToFile(f"Successfully moved to trash: {itemPath}")
if result != 1:
logging.writeToFile(f"Failed to move to trash {itemPath}: result={result}, trying Python fallback")
# Fallback: Python+base64 to handle special chars in paths (e.g. lscpd/sendCommand)
if not self._moveViaPythonBase64(itemPath, trashPath, website.externalApp):
return self.ajaxPre(0, f'Failed to move {item} to trash')
logging.writeToFile(f"Successfully moved to trash: {itemPath}")
## Update disk usage in background
command = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/IncScheduler.py UpdateDiskUsageForceDomain --domainName %s" % (domainName)
ProcessUtilities.popenExecutioner(command)
if RemoveOK == 0:
logging.CyberCPLogFileWriter.writeToFile(f"Restoring chattr +i flags for {self.homePath}")
logging.writeToFile(f"Restoring chattr +i flags for {self.homePath}")
# Restore immutable flag to entire directory
# Restore immutable flag to entire directory (executioner returns 1=success, 0=failure)
command = 'chattr -R +i %s' % (self.homePath)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to restore chattr +i to {self.homePath}: {result}")
if result != 1:
logging.writeToFile(f"Warning: Failed to restore chattr +i to {self.homePath}: result={result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully restored chattr +i to {self.homePath}")
logging.writeToFile(f"Successfully restored chattr +i to {self.homePath}")
# Allow specific directories to remain mutable
mutable_dirs = ['/logs/', '/.trash/', '/backup/', '/incbackup/', '/lscache/', '/.cagefs/']
@@ -494,68 +546,74 @@ class FileManager:
dir_path = self.homePath + dir_name
command = 'chattr -R -i %s' % (dir_path)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr +i from {dir_path}: {result}")
if result != 1:
logging.writeToFile(f"Warning: Failed to remove chattr +i from {dir_path}: result={result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr +i from {dir_path}")
logging.writeToFile(f"Successfully removed chattr +i from {dir_path}")
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error in deleteFolderOrFile for {domainName}: {str(e)}")
import traceback
logging.writeToFile(f"Error in deleteFolderOrFile for {domainName}: {str(e)}")
logging.writeToFile(traceback.format_exc())
try:
skipTrash = self.data['skipTrash']
except:
skipTrash = False
# Fallback to root path for system files
# Fallback to root path for system files (Root File Manager, domainName empty)
self.homePath = '/'
logging.CyberCPLogFileWriter.writeToFile(f"Using fallback deletion for system files in {self.data['path']}")
logging.writeToFile(f"Using fallback deletion for system files in {self.data['path']}")
RemoveOK = 1
# Test if directory is writable
command = 'touch %s/public_html/hello.txt' % (self.homePath)
# Test if we can write (use /tmp for root path since /public_html doesn't exist at /)
test_path = '/tmp' if self.homePath == '/' else (self.homePath + '/public_html')
command = 'touch %s/hello.txt' % (test_path)
result = ProcessUtilities.outputExecutioner(command)
if result is None:
result = ''
if result.find('cannot touch') > -1:
if isinstance(result, (str, bytes)) and ('cannot touch' in str(result) or 'Permission denied' in str(result)):
RemoveOK = 0
logging.CyberCPLogFileWriter.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
logging.writeToFile(f"Directory {self.homePath} is not writable, removing chattr flags")
command = 'chattr -R -i %s' % (self.homePath)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
if result != 1:
logging.writeToFile(f"Warning: Failed to remove chattr -i from {self.homePath}: {result}")
else:
command = 'rm -f %s/public_html/hello.txt' % (self.homePath)
command = 'rm -f %s/hello.txt' % (test_path)
ProcessUtilities.executioner(command)
for item in self.data['fileAndFolders']:
itemPath = self.data['path'] + '/' + item
base = self.data['path'].rstrip('/') or '/'
itemPath = base + '/' + item
# Security check for system files
if itemPath.find('..') > -1 or itemPath.find(self.homePath) == -1:
logging.CyberCPLogFileWriter.writeToFile(f"Security violation: Attempted to delete outside allowed path: {itemPath}")
if not ACLManager.isPathInsideHome(itemPath, self.homePath):
logging.writeToFile(f"Security violation: Attempted to delete outside allowed path: {itemPath}")
return self.ajaxPre(0, 'Not allowed to delete files outside allowed path!')
logging.CyberCPLogFileWriter.writeToFile(f"Deleting system file: {itemPath}")
logging.writeToFile(f"Deleting system file: {itemPath}")
if skipTrash:
command = 'rm -rf ' + self.returnPathEnclosed(itemPath)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1 or result.find('Permission denied') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Failed to delete system file {itemPath}: {result}")
return self.ajaxPre(0, f'Failed to delete {item}: {result}')
logging.CyberCPLogFileWriter.writeToFile(f"Successfully deleted system file: {itemPath}")
if result != 1:
logging.writeToFile(f"Failed to delete system file {itemPath}: result={result}, trying Python fallback")
if not self._deleteViaPythonBase64(itemPath, None):
return self.ajaxPre(0, f'Failed to delete {item}')
logging.writeToFile(f"Successfully deleted system file: {itemPath}")
if RemoveOK == 0:
logging.CyberCPLogFileWriter.writeToFile(f"Restoring chattr +i flags for system path: {self.homePath}")
logging.writeToFile(f"Restoring chattr +i flags for system path: {self.homePath}")
command = 'chattr -R +i %s' % (self.homePath)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to restore chattr +i to system path {self.homePath}: {result}")
if result != 1:
logging.writeToFile(f"Warning: Failed to restore chattr +i to system path {self.homePath}: result={result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully restored chattr +i to system path {self.homePath}")
logging.writeToFile(f"Successfully restored chattr +i to system path {self.homePath}")
# Allow specific directories to remain mutable for system files
mutable_dirs = ['/logs/', '/.trash/', '/backup/', '/incbackup/', '/lscache/', '/.cagefs/']
@@ -563,17 +621,17 @@ class FileManager:
dir_path = self.homePath + dir_name
command = 'chattr -R -i %s' % (dir_path)
result = ProcessUtilities.executioner(command)
if result.find('cannot') > -1:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove chattr +i from system {dir_path}: {result}")
if result != 1:
logging.writeToFile(f"Warning: Failed to remove chattr +i from system {dir_path}: result={result}")
else:
logging.CyberCPLogFileWriter.writeToFile(f"Successfully removed chattr +i from system {dir_path}")
logging.writeToFile(f"Successfully removed chattr +i from system {dir_path}")
logging.CyberCPLogFileWriter.writeToFile(f"File deletion completed successfully for domain: {domainName}")
logging.writeToFile(f"File deletion completed successfully for domain: {domainName}")
json_data = json.dumps(finalData)
return HttpResponse(json_data)
except BaseException as msg:
logging.CyberCPLogFileWriter.writeToFile(f"Critical error in deleteFolderOrFile: {str(msg)}")
logging.writeToFile(f"Critical error in deleteFolderOrFile: {str(msg)}")
return self.ajaxPre(0, f"File deletion failed: {str(msg)}")
def restore(self):
@@ -593,8 +651,7 @@ class FileManager:
for item in self.data['fileAndFolders']:
if (self.data['path'] + '/' + item).find('..') > -1 or (self.data['path'] + '/' + item).find(
self.homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['path'] + '/' + item, self.homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
trashPath = '%s/.trash' % (self.homePath)
@@ -628,13 +685,12 @@ class FileManager:
homePath = '/home/%s' % (domainName)
if self.data['newPath'].find('..') > -1 or self.data['newPath'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['newPath'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if len(self.data['fileAndFolders']) == 1:
if (self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find('..') > -1 or (
self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['fileAndFolders'][0], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = 'yes| cp -Rf %s %s' % (
@@ -654,8 +710,7 @@ class FileManager:
ProcessUtilities.executioner(command, website.externalApp)
for item in self.data['fileAndFolders']:
if (self.data['basePath'] + '/' + item).find('..') > -1 or (self.data['basePath'] + '/' + item).find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = '%scp -Rf ' % ('yes |') + self.returnPathEnclosed(
@@ -672,13 +727,12 @@ class FileManager:
homePath = '/'
if self.data['newPath'].find('..') > -1 or self.data['newPath'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['newPath'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if len(self.data['fileAndFolders']) == 1:
if (self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find('..') > -1 or (
self.data['basePath'] + '/' + self.data['fileAndFolders'][0]).find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['fileAndFolders'][0], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = 'yes| cp -Rf %s %s' % (
@@ -698,9 +752,7 @@ class FileManager:
ProcessUtilities.executioner(command)
for item in self.data['fileAndFolders']:
if (self.data['basePath'] + '/' + item).find('..') > -1 or (
self.data['basePath'] + '/' + item).find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = '%scp -Rf ' % ('yes |') + self.returnPathEnclosed(
@@ -735,12 +787,10 @@ class FileManager:
for item in self.data['fileAndFolders']:
if (self.data['basePath'] + '/' + item).find('..') > -1 or (self.data['basePath'] + '/' + item).find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if (self.data['newPath'] + '/' + item).find('..') > -1 or (self.data['newPath'] + '/' + item).find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['newPath'] + '/' + item, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = 'mv ' + self.returnPathEnclosed(
@@ -765,13 +815,10 @@ class FileManager:
for item in self.data['fileAndFolders']:
if (self.data['basePath'] + '/' + item).find('..') > -1 or (
self.data['basePath'] + '/' + item).find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + item, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if (self.data['newPath'] + '/' + item).find('..') > -1 or (self.data['newPath'] + '/' + item).find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['newPath'] + '/' + item, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = 'mv ' + self.returnPathEnclosed(
@@ -803,11 +850,10 @@ class FileManager:
homePath = '/home/%s' % (domainName)
if (self.data['basePath'] + '/' + self.data['existingName']).find('..') > -1 or (
self.data['basePath'] + '/' + self.data['existingName']).find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['existingName'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if (self.data['newFileName']).find('..') > -1 or (self.data['basePath']).find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['newFileName'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = 'mv ' + self.returnPathEnclosed(
@@ -819,11 +865,10 @@ class FileManager:
except:
homePath = '/'
if (self.data['basePath'] + '/' + self.data['existingName']).find('..') > -1 or (
self.data['basePath'] + '/' + self.data['existingName']).find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['existingName'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if (self.data['newFileName']).find('..') > -1 or (self.data['basePath']).find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + '/' + self.data['newFileName'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = 'mv ' + self.returnPathEnclosed(
@@ -850,7 +895,7 @@ class FileManager:
pathCheck = '/home/%s' % (domainName)
if self.data['fileName'].find(pathCheck) == -1 or self.data['fileName'].find('..') > -1:
if not ACLManager.isPathInsideHome(self.data['fileName'], pathCheck):
return self.ajaxPre(0, 'Not allowed.')
# Ensure proper UTF-8 handling for file reading
@@ -860,7 +905,7 @@ class FileManager:
except:
pathCheck = '/'
if self.data['fileName'].find(pathCheck) == -1 or self.data['fileName'].find('..') > -1:
if not ACLManager.isPathInsideHome(self.data['fileName'], pathCheck):
return self.ajaxPre(0, 'Not allowed.')
# Ensure proper UTF-8 handling for file reading
@@ -958,11 +1003,11 @@ class FileManager:
if result.find('->') > -1:
return self.ajaxPre(0, "Symlink attack.")
if ACLManager.commandInjectionCheck(self.data['completePath'] + '/' + myfile.name) == 1:
uploadPathFull = self.data['completePath'] + '/' + myfile.name
if not ACLManager.isFilePathSafeForShell(uploadPathFull):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if (self.data['completePath'] + '/' + myfile.name).find(pathCheck) == -1 or (
(self.data['completePath'] + '/' + myfile.name)).find('..') > -1:
if not ACLManager.isPathInsideHome(uploadPathFull, pathCheck):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = 'cp ' + self.returnPathEnclosed(
@@ -985,11 +1030,11 @@ class FileManager:
command = 'ls -la %s' % (self.data['completePath'])
result = ProcessUtilities.outputExecutioner(command)
logging.writeToFile("upload file res %s" % result)
if ACLManager.commandInjectionCheck(self.data['completePath'] + '/' + myfile.name) == 1:
uploadPathFull = self.data['completePath'] + '/' + myfile.name
if not ACLManager.isFilePathSafeForShell(uploadPathFull):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if (self.data['completePath'] + '/' + myfile.name).find(pathCheck) == -1 or (
(self.data['completePath'] + '/' + myfile.name)).find('..') > -1:
if not ACLManager.isPathInsideHome(uploadPathFull, pathCheck):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = 'cp ' + self.returnPathEnclosed(
@@ -1034,10 +1079,10 @@ class FileManager:
homePath = '/home/%s' % (domainName)
if self.data['extractionLocation'].find('..') > -1 or self.data['extractionLocation'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['extractionLocation'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if self.data['fileToExtract'].find('..') > -1 or self.data['fileToExtract'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['fileToExtract'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if self.data['extractionType'] == 'zip':
@@ -1065,11 +1110,10 @@ class FileManager:
homePath = '/'
if self.data['extractionLocation'].find('..') > -1 or self.data['extractionLocation'].find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['extractionLocation'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if self.data['fileToExtract'].find('..') > -1 or self.data['fileToExtract'].find(homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['fileToExtract'], homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
if self.data['extractionType'] == 'zip':
@@ -1130,8 +1174,7 @@ class FileManager:
for item in self.data['listOfFiles']:
if (self.data['basePath'] + item).find('..') > -1 or (self.data['basePath'] + item).find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + item, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = '%s%s ' % (command, self.returnPathEnclosed(item))
@@ -1168,8 +1211,7 @@ class FileManager:
for item in self.data['listOfFiles']:
if (self.data['basePath'] + item).find('..') > -1 or (self.data['basePath'] + item).find(
homePath) == -1:
if not ACLManager.isPathInsideHome(self.data['basePath'] + item, homePath):
return self.ajaxPre(0, 'Not allowed to move in this path, please choose location inside home!')
command = '%s%s ' % (command, self.returnPathEnclosed(item))

View File

@@ -82,6 +82,15 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
$scope.showUploadBox = function () {
$('#uploadBox').modal('show');
};
// Fix aria-hidden a11y: move focus out of modal before hide so no focused descendant retains focus
$(document).on('hide.bs.modal', '.modal', function () {
var modal = this;
if (document.activeElement && modal.contains(document.activeElement)) {
var trigger = document.getElementById('uploadTriggerBtn');
if (trigger && modal.id === 'uploadBox') { trigger.focus(); }
else { document.activeElement.blur(); }
}
});
$scope.showHTMLEditorModal = function (MainFM= 0) {
$scope.htmlEditorLoading = false;
@@ -1147,7 +1156,8 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
});
$scope.fetchForTableSecondary(null, 'refresh');
} else {
var notification = alertify.notify('Files/Folders can not be deleted', 'error', 5, function () {
var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Files/Folders can not be deleted';
var notification = alertify.notify(errMsg, 'error', 8, function () {
console.log('dismissed');
});
}
@@ -1155,6 +1165,10 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader,
}
function cantLoadInitialDatas(response) {
var err = (response && response.data && (response.data.error_message || response.data.message)) ||
(response && response.statusText) || 'Request failed';
if (response && response.status === 0) err = 'Network error';
alertify.notify(err, 'error', 8);
}
};

View File

@@ -156,6 +156,14 @@ function findFileExtension(fileName) {
$scope.showUploadBox = function () {
$("#uploadBox").modal();
};
$(document).on("hide.bs.modal", ".modal", function () {
var modal = this;
if (document.activeElement && modal.contains(document.activeElement)) {
var trigger = document.getElementById("uploadTriggerBtn");
if (trigger && modal.id === "uploadBox") { trigger.focus(); }
else { document.activeElement.blur(); }
}
});
$scope.showHTMLEditorModal = function (MainFM = 0) {
$scope.fileInEditor = allFilesAndFolders[0];

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/png" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'filemanager/images/fonts/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'filemanager/css/fileManager.css' %}">
@@ -186,7 +186,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"
integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/js/bootstrap.min.js"
integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ"
crossorigin="anonymous"></script>

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/png" href="{% static 'baseTemplate/assets/finalBase/favicon.png' %}">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'filemanager/images/fonts/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'filemanager/css/fileManager.css' %}">
@@ -60,7 +60,7 @@
</div-->
<ul class="nav mr-10">
<li class="nav-item">
<a onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
<a id="uploadTriggerBtn" onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#"><i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}</a>
</li>
<li class="nav-item">
<a onclick="return false;" ng-click="showCreateFileModal()" class="nav-link point-events" href="#"><i class="fa fa-plus-square" aria-hidden="true"></i> {% trans "New File" %}</a>
@@ -709,7 +709,7 @@
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<!-- HTML Editor Include -->

View File

@@ -439,7 +439,7 @@
<div class="fm-toolbar">
<ul class="nav">
<li class="nav-item">
<a onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#">
<a id="uploadTriggerBtn" onclick="return false;" ng-click="showUploadBox()" class="nav-link point-events" href="#">
<i class="fa fa-upload" aria-hidden="true"></i> {% trans "Upload" %}
</a>
</li>

View File

@@ -1001,7 +1001,7 @@
</div>
<div class="fm-toolbar-group">
<button class="fm-btn primary" ng-click="showUploadBox()">
<button id="uploadTriggerBtn" class="fm-btn primary" ng-click="showUploadBox()">
<i class="fas fa-cloud-upload-alt"></i>
{% trans "Upload" %}
</button>

View File

@@ -210,11 +210,18 @@ def upload(request):
admin = Administrator.objects.get(pk=userID)
currentACL = ACLManager.loadedACL(userID)
if ACLManager.checkOwnership(data['domainName'], admin, currentACL) == 1:
domainName = data.get('domainName', '')
if domainName == '':
# Root File Manager: allow only admin
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
elif ACLManager.checkOwnership(domainName, admin, currentACL) == 1:
pass
else:
return ACLManager.loadErrorJson()
except:
except Exception:
return ACLManager.loadErrorJson()
fm = FM(request, data)

28
install/safe-delete-path Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Helper script for CyberPanel File Manager: delete a file/dir by base64-encoded path.
Invoked by lscpd as root. Exits 0 on success, 1 on failure."""
import sys
import os
import base64
import shutil
def main():
if len(sys.argv) < 2:
sys.exit(1)
try:
p_b64 = sys.argv[1]
path = base64.b64decode(p_b64).decode('utf-8')
if not path or not os.path.isabs(path):
sys.exit(1)
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
shutil.rmtree(path)
else:
sys.exit(1)
sys.exit(0)
except Exception:
sys.exit(1)
if __name__ == '__main__':
main()

24
install/safe-move-path Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Helper script for CyberPanel File Manager: move src to dest by base64-encoded paths.
Invoked by lscpd as root. Exits 0 on success, 1 on failure."""
import sys
import os
import base64
import shutil
def main():
if len(sys.argv) < 3:
sys.exit(1)
try:
s_b64, d_b64 = sys.argv[1], sys.argv[2]
src = base64.b64decode(s_b64).decode('utf-8')
dest = base64.b64decode(d_b64).decode('utf-8')
if not src or not dest or not os.path.isabs(src):
sys.exit(1)
shutil.move(src, dest)
sys.exit(0)
except Exception:
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -154,6 +154,41 @@ class ACLManager:
except BaseException as msg:
logging.writeToFile('%s. [32:commandInjectionCheck]' % (str(msg)))
@staticmethod
def isPathInsideHome(path, home_path):
"""
Check if path is inside the allowed home directory. Uses normpath to correctly
allow filenames like 'file..name.txt' while rejecting path traversal (e.g. ../../etc).
"""
try:
if not path or not isinstance(path, str):
return False
path = os.path.normpath(path)
if not os.path.isabs(path):
return False
base = os.path.realpath(home_path)
if base == '/':
return True
return path == base or path.startswith(base + os.sep)
except (OSError, TypeError) as msg:
logging.writeToFile('%s. [isPathInsideHome]' % (str(msg)))
return False
@staticmethod
def isFilePathSafeForShell(path):
"""
Check if path is safe for shell when passed in single quotes. Only blocks
characters that break single-quoted strings: quote, null, newline.
Allows ( ) : & [ ] etc. since they are harmless inside single quotes.
"""
try:
if not path or not isinstance(path, str):
return False
return "'" not in path and '\0' not in path and '\n' not in path
except (TypeError, AttributeError) as msg:
logging.writeToFile('%s. [isFilePathSafeForShell]' % (str(msg)))
return False
@staticmethod
def loadedACL(val):