mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-03-18 02:00:16 +01:00
Security fixes for webmail and emailDelivery apps
- Fix command injection in relay config: use shlex.quote() on all subprocess arguments passed to mailUtilities.py - Fix XSS in email reply/forward: html.escape() on From/To/Date/Subject headers before embedding in quoted HTML - Fix attachment filename traversal: use os.path.basename() and strip null bytes from attachment filenames - Fix Sieve script name injection: sanitize names to alphanumeric chars - Fix SSRF in image proxy: resolve hostname to IP and check against ipaddress.is_private/is_loopback/is_link_local/is_reserved - Remove internal error details from user-facing responses - Update Access Webmail link from /snappymail/ to /webmail/
This commit is contained in:
@@ -61,7 +61,6 @@ class EmailDeliveryManager:
|
||||
except Exception as e:
|
||||
self.logger.writeToFile('[EmailDeliveryManager.home] Error: %s' % str(e))
|
||||
proc = httpProc(request, 'emailDelivery/index.html', {
|
||||
'error': str(e),
|
||||
'isConnected': False,
|
||||
'adminEmail': '',
|
||||
'adminName': '',
|
||||
@@ -578,9 +577,13 @@ class EmailDeliveryManager:
|
||||
smtpPassword = result.get('data', {}).get('new_password', '')
|
||||
|
||||
# Configure Postfix relay via mailUtilities subprocess
|
||||
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/mailUtilities.py"
|
||||
execPath += " configureRelayHost --smtpHost %s --smtpPort %s --smtpUser '%s' --smtpPassword '%s'" % (
|
||||
account.smtp_host, account.smtp_port, account.smtp_username, smtpPassword
|
||||
import shlex
|
||||
execPath = "/usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/mailUtilities.py" \
|
||||
" configureRelayHost --smtpHost %s --smtpPort %s --smtpUser %s --smtpPassword %s" % (
|
||||
shlex.quote(str(account.smtp_host)),
|
||||
shlex.quote(str(account.smtp_port)),
|
||||
shlex.quote(str(account.smtp_username)),
|
||||
shlex.quote(str(smtpPassword))
|
||||
)
|
||||
output = ProcessUtilities.outputExecutioner(execPath)
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 btn-min-width">
|
||||
<a href="/snappymail/index.php" title="{% trans 'Access Webmail' %}"
|
||||
<a href="/webmail/" title="{% trans 'Access Webmail' %}"
|
||||
class="tile-box tile-box-shortcut btn-primary">
|
||||
<div class="tile-header">
|
||||
{% trans "Access Webmail" %}
|
||||
|
||||
@@ -118,10 +118,11 @@ class EmailComposer:
|
||||
references = ('%s %s' % (references, in_reply_to)).strip()
|
||||
|
||||
# Quote original
|
||||
orig_date = original.get('date', '')
|
||||
orig_from = original.get('from', '')
|
||||
from html import escape as html_escape
|
||||
orig_date = html_escape(original.get('date', ''))
|
||||
orig_from = html_escape(original.get('from', ''))
|
||||
quoted = '<br><br><div class="wm-quoted">On %s, %s wrote:<br><blockquote>%s</blockquote></div>' % (
|
||||
orig_date, orig_from, original.get('body_html', '') or original.get('body_text', '')
|
||||
orig_date, orig_from, original.get('body_html', '') or html_escape(original.get('body_text', ''))
|
||||
)
|
||||
full_html = body_html + quoted
|
||||
|
||||
@@ -152,10 +153,11 @@ class EmailComposer:
|
||||
if not subject.lower().startswith('fwd:'):
|
||||
subject = 'Fwd: %s' % subject
|
||||
|
||||
orig_from = original.get('from', '')
|
||||
orig_to = original.get('to', '')
|
||||
orig_date = original.get('date', '')
|
||||
orig_subject = original.get('subject', '')
|
||||
from html import escape as html_escape
|
||||
orig_from = html_escape(original.get('from', ''))
|
||||
orig_to = html_escape(original.get('to', ''))
|
||||
orig_date = html_escape(original.get('date', ''))
|
||||
orig_subject = html_escape(original.get('subject', ''))
|
||||
|
||||
forwarded = (
|
||||
'<br><br><div class="wm-forwarded">'
|
||||
@@ -166,7 +168,7 @@ class EmailComposer:
|
||||
'To: %s<br><br>'
|
||||
'%s</div>'
|
||||
) % (orig_from, orig_date, orig_subject, orig_to,
|
||||
original.get('body_html', '') or original.get('body_text', ''))
|
||||
original.get('body_html', '') or html_escape(original.get('body_text', '')))
|
||||
|
||||
full_html = body_html + forwarded
|
||||
|
||||
|
||||
@@ -104,9 +104,18 @@ class SieveClient:
|
||||
scripts.append((match.group(1), bool(match.group(2))))
|
||||
return scripts
|
||||
|
||||
@staticmethod
|
||||
def _safe_name(name):
|
||||
"""Sanitize script name to prevent ManageSieve injection."""
|
||||
import re
|
||||
safe = re.sub(r'[^a-zA-Z0-9_.-]', '', name)
|
||||
if not safe:
|
||||
safe = 'default'
|
||||
return safe
|
||||
|
||||
def get_script(self, name):
|
||||
"""Get the content of a Sieve script."""
|
||||
self._send('GETSCRIPT "%s"' % name)
|
||||
self._send('GETSCRIPT "%s"' % self._safe_name(name))
|
||||
ok, lines, _ = self._read_response()
|
||||
if not ok:
|
||||
return ''
|
||||
@@ -114,8 +123,9 @@ class SieveClient:
|
||||
|
||||
def put_script(self, name, content):
|
||||
"""Upload a Sieve script."""
|
||||
safe = self._safe_name(name)
|
||||
encoded = content.encode('utf-8')
|
||||
self._send('PUTSCRIPT "%s" {%d+}' % (name, len(encoded)))
|
||||
self._send('PUTSCRIPT "%s" {%d+}' % (safe, len(encoded)))
|
||||
self.sock.sendall(encoded + b'\r\n')
|
||||
ok, _, msg = self._read_response()
|
||||
if not ok:
|
||||
@@ -124,7 +134,7 @@ class SieveClient:
|
||||
|
||||
def activate_script(self, name):
|
||||
"""Set a script as the active script."""
|
||||
self._send('SETACTIVE "%s"' % name)
|
||||
self._send('SETACTIVE "%s"' % self._safe_name(name))
|
||||
ok, _, msg = self._read_response()
|
||||
return ok
|
||||
|
||||
@@ -136,7 +146,7 @@ class SieveClient:
|
||||
|
||||
def delete_script(self, name):
|
||||
"""Delete a Sieve script."""
|
||||
self._send('DELETESCRIPT "%s"' % name)
|
||||
self._send('DELETESCRIPT "%s"' % self._safe_name(name))
|
||||
ok, _, _ = self._read_response()
|
||||
return ok
|
||||
|
||||
|
||||
@@ -325,8 +325,12 @@ class WebmailManager:
|
||||
return self._error('Attachment not found.')
|
||||
filename, content_type, payload = result
|
||||
response = HttpResponse(payload, content_type=content_type)
|
||||
# Sanitize filename to prevent header injection
|
||||
safe_filename = filename.replace('"', '_').replace('\r', '').replace('\n', '')
|
||||
# Sanitize filename to prevent header injection and path traversal
|
||||
import os as _os
|
||||
safe_filename = _os.path.basename(filename)
|
||||
safe_filename = safe_filename.replace('"', '_').replace('\r', '').replace('\n', '').replace('\x00', '')
|
||||
if not safe_filename:
|
||||
safe_filename = 'attachment'
|
||||
response['Content-Disposition'] = 'attachment; filename="%s"' % safe_filename
|
||||
return response
|
||||
except Exception as e:
|
||||
@@ -794,12 +798,15 @@ class WebmailManager:
|
||||
|
||||
# Block internal/private IPs to prevent SSRF
|
||||
import urllib.parse
|
||||
import socket
|
||||
import ipaddress
|
||||
hostname = urllib.parse.urlparse(url).hostname or ''
|
||||
if hostname in ('localhost', '127.0.0.1', '::1', '0.0.0.0') or \
|
||||
hostname.startswith(('10.', '192.168.', '172.16.', '172.17.', '172.18.',
|
||||
'172.19.', '172.20.', '172.21.', '172.22.', '172.23.',
|
||||
'172.24.', '172.25.', '172.26.', '172.27.', '172.28.',
|
||||
'172.29.', '172.30.', '172.31.', '169.254.')):
|
||||
try:
|
||||
resolved_ip = socket.gethostbyname(hostname)
|
||||
ip_obj = ipaddress.ip_address(resolved_ip)
|
||||
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_multicast or ip_obj.is_reserved:
|
||||
return self._error('Invalid URL.')
|
||||
except (socket.gaierror, ValueError):
|
||||
return self._error('Invalid URL.')
|
||||
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user