From 855f9a3a6d5f95c3d7ef0c7714579a95f71120ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bot=20of=20Thomas=20G=C3=BCttler?= Date: Wed, 13 May 2026 23:49:30 +0200 Subject: [PATCH] feat(S2): validate IMAP/SMTP hostnames against injection (#25) --- lib/core/utils/host_utils.dart | 18 ++++++++++++++++++ lib/ui/screens/add_account_screen.dart | 12 +++++++----- lib/ui/screens/edit_account_screen.dart | 13 ++++++++----- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/lib/core/utils/host_utils.dart b/lib/core/utils/host_utils.dart index 2799ce8..b775efc 100644 --- a/lib/core/utils/host_utils.dart +++ b/lib/core/utils/host_utils.dart @@ -2,3 +2,21 @@ bool isLocalhost(String host) { final h = host.trim().toLowerCase(); return h == 'localhost' || h == '127.0.0.1' || h == '::1'; } + +String? validateHostname(String? value) { + if (value == null || value.trim().isEmpty) return 'Required'; + return _checkHostChars(value.trim()); +} + +String? validateOptionalHostname(String? value) { + if (value == null || value.trim().isEmpty) return null; + return _checkHostChars(value.trim()); +} + +String? _checkHostChars(String h) { + if (h.contains(RegExp(r'[@/\\]')) || + h.codeUnits.any((c) => c < 32 || c == 127)) { + return 'Invalid hostname'; + } + return null; +} diff --git a/lib/ui/screens/add_account_screen.dart b/lib/ui/screens/add_account_screen.dart index 039dba3..71918ea 100644 --- a/lib/ui/screens/add_account_screen.dart +++ b/lib/ui/screens/add_account_screen.dart @@ -408,7 +408,7 @@ class _AddAccountScreenState extends ConsumerState { _field(_passwordCtrl, 'Password', obscure: true), const Divider(height: 32), Text('IMAP', style: Theme.of(context).textTheme.titleSmall), - _field(_imapHostCtrl, 'Host'), + _field(_imapHostCtrl, 'Host', validator: validateHostname), _field(_imapPortCtrl, 'Port', keyboardType: TextInputType.number), if (isLocalhost(_imapHostCtrl.text.trim())) SwitchListTile( @@ -418,7 +418,7 @@ class _AddAccountScreenState extends ConsumerState { ), const Divider(height: 32), Text('SMTP', style: Theme.of(context).textTheme.titleSmall), - _field(_smtpHostCtrl, 'Host'), + _field(_smtpHostCtrl, 'Host', validator: validateHostname), _field(_smtpPortCtrl, 'Port', keyboardType: TextInputType.number), if (isLocalhost(_smtpHostCtrl.text.trim())) SwitchListTile( @@ -475,6 +475,7 @@ class _AddAccountScreenState extends ConsumerState { bool obscure = false, bool required = true, TextInputType? keyboardType, + String? Function(String?)? validator, }) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), @@ -486,9 +487,10 @@ class _AddAccountScreenState extends ConsumerState { labelText: label, border: const OutlineInputBorder(), ), - validator: required - ? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null - : null, + validator: validator ?? + (required + ? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null + : null), ), ); } diff --git a/lib/ui/screens/edit_account_screen.dart b/lib/ui/screens/edit_account_screen.dart index 386cb43..2071f6e 100644 --- a/lib/ui/screens/edit_account_screen.dart +++ b/lib/ui/screens/edit_account_screen.dart @@ -324,11 +324,11 @@ class _EditAccountScreenState extends ConsumerState { 'IMAP (SSL/TLS)', style: Theme.of(context).textTheme.titleSmall, ), - _field(_imapHostCtrl, 'Host'), + _field(_imapHostCtrl, 'Host', validator: validateHostname), _field(_imapPortCtrl, 'Port', keyboardType: TextInputType.number), const Divider(height: 32), Text('SMTP', style: Theme.of(context).textTheme.titleSmall), - _field(_smtpHostCtrl, 'Host'), + _field(_smtpHostCtrl, 'Host', validator: validateHostname), _field(_smtpPortCtrl, 'Port', keyboardType: TextInputType.number), if (isLocalhost(_smtpHostCtrl.text.trim())) SwitchListTile( @@ -348,6 +348,7 @@ class _EditAccountScreenState extends ConsumerState { _sieveHostCtrl, 'Host (leave blank to use IMAP host)', required: false, + validator: validateOptionalHostname, ), _field( _sievePortCtrl, @@ -408,6 +409,7 @@ class _EditAccountScreenState extends ConsumerState { bool obscure = false, bool required = true, TextInputType? keyboardType, + String? Function(String?)? validator, }) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), @@ -420,9 +422,10 @@ class _EditAccountScreenState extends ConsumerState { labelText: label, border: const OutlineInputBorder(), ), - validator: required - ? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null - : null, + validator: validator ?? + (required + ? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null + : null), ), ); }