mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-07-05 06:48:50 +02:00
Fix critical webmail bugs: XSS, SSRF, install ordering, and UI issues
Security fixes: - Escape plain text body to prevent XSS via trustAsHtml - Add SSRF protection to image proxy (block private IPs, require auth) - Sanitize Content-Disposition filename to prevent header injection - Escape Sieve script values to prevent script injection - Escape IMAP search query to prevent search injection Install/upgrade fixes: - Move setupWebmail() call to after Dovecot is installed (was running before doveadm existed, silently failing on every fresh install) - Make setupWebmail() a static method callable from install.py - Fix upgrade idempotency: always run dovecot.conf patching and migrations even if webmail.conf already exists (partial failure recovery) Frontend fixes: - Fix search being a no-op (was ignoring results and just reloading) - Fix loading spinner stuck forever on API errors (add errback) - Fix unread count decrementing on already-read messages - Fix draft auto-save timer leak when navigating away from compose - Fix composeToContact missing signature and auto-save - Fix null subject crash in reply/forward - Clear stale data when switching accounts - Fix attachment part_id mismatch between parser and downloader Backend fixes: - Fix Sieve _read_response infinite loop on connection drop - Add login check to apiSaveDraft
This commit is contained in:
@@ -209,7 +209,9 @@ class IMAPClient:
|
||||
def search_messages(self, folder='INBOX', query='', criteria='ALL'):
|
||||
self._select(folder)
|
||||
if query:
|
||||
search_criteria = '(OR OR (FROM "%s") (TO "%s") (SUBJECT "%s"))' % (query, query, query)
|
||||
# Escape quotes to prevent IMAP search injection
|
||||
safe_query = query.replace('\\', '\\\\').replace('"', '\\"')
|
||||
search_criteria = '(OR OR (FROM "%s") (TO "%s") (SUBJECT "%s"))' % (safe_query, safe_query, safe_query)
|
||||
else:
|
||||
search_criteria = criteria
|
||||
status, data = self.conn.uid('search', None, search_criteria)
|
||||
@@ -263,13 +265,16 @@ class IMAPClient:
|
||||
msg = email.message_from_bytes(raw) if isinstance(raw, bytes) else email.message_from_string(raw)
|
||||
part_idx = 0
|
||||
for part in msg.walk():
|
||||
if part.get_content_maintype() == 'multipart':
|
||||
content_type = part.get_content_type()
|
||||
if content_type.startswith('multipart/'):
|
||||
continue
|
||||
if part.get('Content-Disposition') and 'attachment' in part.get('Content-Disposition', ''):
|
||||
disposition = str(part.get('Content-Disposition', ''))
|
||||
# Match the same indexing logic as email_parser.py:
|
||||
# count parts that are attachments or non-text with disposition
|
||||
if 'attachment' in disposition or (content_type not in ('text/html', 'text/plain') and disposition):
|
||||
if str(part_idx) == str(part_id):
|
||||
filename = part.get_filename() or 'attachment'
|
||||
filename = self._decode_header_value(filename)
|
||||
content_type = part.get_content_type()
|
||||
payload = part.get_payload(decode=True)
|
||||
return (filename, content_type, payload)
|
||||
part_idx += 1
|
||||
|
||||
@@ -39,6 +39,8 @@ class SieveClient:
|
||||
lines = []
|
||||
while True:
|
||||
line = self._read_line()
|
||||
if not line and not self.buf:
|
||||
return False, lines, 'Connection closed'
|
||||
if line.startswith('OK'):
|
||||
return True, lines, line
|
||||
elif line.startswith('NO'):
|
||||
@@ -167,9 +169,9 @@ class SieveClient:
|
||||
for rule in rules:
|
||||
field = rule.get('condition_field', 'from')
|
||||
cond_type = rule.get('condition_type', 'contains')
|
||||
cond_value = rule.get('condition_value', '')
|
||||
cond_value = rule.get('condition_value', '').replace('\\', '\\\\').replace('"', '\\"')
|
||||
action_type = rule.get('action_type', 'move')
|
||||
action_value = rule.get('action_value', '')
|
||||
action_value = rule.get('action_value', '').replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
# Map field to Sieve header
|
||||
if field == 'from':
|
||||
|
||||
Reference in New Issue
Block a user