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:
usmannasir
2026-03-05 05:10:14 +05:00
parent 6a61e294a9
commit 632dc3fbe9
7 changed files with 137 additions and 59 deletions

View File

@@ -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

View File

@@ -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':