Compare commits

...
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 c4634936ae feat(S2): validate IMAP/SMTP hostnames against injection characters
Add validateHostname / validateOptionalHostname helpers to host_utils.dart
that reject values containing @, /, \, or control characters. Wire them
into AddAccountScreen and EditAccountScreen for all host fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 23:45:37 +02:00
3 changed files with 33 additions and 10 deletions
+18
View File
@@ -2,3 +2,21 @@ bool isLocalhost(String host) {
final h = host.trim().toLowerCase(); final h = host.trim().toLowerCase();
return h == 'localhost' || h == '127.0.0.1' || h == '::1'; 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;
}
+7 -5
View File
@@ -408,7 +408,7 @@ class _AddAccountScreenState extends ConsumerState<AddAccountScreen> {
_field(_passwordCtrl, 'Password', obscure: true), _field(_passwordCtrl, 'Password', obscure: true),
const Divider(height: 32), const Divider(height: 32),
Text('IMAP', style: Theme.of(context).textTheme.titleSmall), Text('IMAP', style: Theme.of(context).textTheme.titleSmall),
_field(_imapHostCtrl, 'Host'), _field(_imapHostCtrl, 'Host', validator: validateHostname),
_field(_imapPortCtrl, 'Port', keyboardType: TextInputType.number), _field(_imapPortCtrl, 'Port', keyboardType: TextInputType.number),
if (isLocalhost(_imapHostCtrl.text.trim())) if (isLocalhost(_imapHostCtrl.text.trim()))
SwitchListTile( SwitchListTile(
@@ -418,7 +418,7 @@ class _AddAccountScreenState extends ConsumerState<AddAccountScreen> {
), ),
const Divider(height: 32), const Divider(height: 32),
Text('SMTP', style: Theme.of(context).textTheme.titleSmall), Text('SMTP', style: Theme.of(context).textTheme.titleSmall),
_field(_smtpHostCtrl, 'Host'), _field(_smtpHostCtrl, 'Host', validator: validateHostname),
_field(_smtpPortCtrl, 'Port', keyboardType: TextInputType.number), _field(_smtpPortCtrl, 'Port', keyboardType: TextInputType.number),
if (isLocalhost(_smtpHostCtrl.text.trim())) if (isLocalhost(_smtpHostCtrl.text.trim()))
SwitchListTile( SwitchListTile(
@@ -475,6 +475,7 @@ class _AddAccountScreenState extends ConsumerState<AddAccountScreen> {
bool obscure = false, bool obscure = false,
bool required = true, bool required = true,
TextInputType? keyboardType, TextInputType? keyboardType,
String? Function(String?)? validator,
}) { }) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(vertical: 6),
@@ -486,9 +487,10 @@ class _AddAccountScreenState extends ConsumerState<AddAccountScreen> {
labelText: label, labelText: label,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
validator: required validator: validator ??
? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null (required
: null, ? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null
: null),
), ),
); );
} }
+8 -5
View File
@@ -324,11 +324,11 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
'IMAP (SSL/TLS)', 'IMAP (SSL/TLS)',
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
_field(_imapHostCtrl, 'Host'), _field(_imapHostCtrl, 'Host', validator: validateHostname),
_field(_imapPortCtrl, 'Port', keyboardType: TextInputType.number), _field(_imapPortCtrl, 'Port', keyboardType: TextInputType.number),
const Divider(height: 32), const Divider(height: 32),
Text('SMTP', style: Theme.of(context).textTheme.titleSmall), Text('SMTP', style: Theme.of(context).textTheme.titleSmall),
_field(_smtpHostCtrl, 'Host'), _field(_smtpHostCtrl, 'Host', validator: validateHostname),
_field(_smtpPortCtrl, 'Port', keyboardType: TextInputType.number), _field(_smtpPortCtrl, 'Port', keyboardType: TextInputType.number),
if (isLocalhost(_smtpHostCtrl.text.trim())) if (isLocalhost(_smtpHostCtrl.text.trim()))
SwitchListTile( SwitchListTile(
@@ -348,6 +348,7 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
_sieveHostCtrl, _sieveHostCtrl,
'Host (leave blank to use IMAP host)', 'Host (leave blank to use IMAP host)',
required: false, required: false,
validator: validateOptionalHostname,
), ),
_field( _field(
_sievePortCtrl, _sievePortCtrl,
@@ -408,6 +409,7 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
bool obscure = false, bool obscure = false,
bool required = true, bool required = true,
TextInputType? keyboardType, TextInputType? keyboardType,
String? Function(String?)? validator,
}) { }) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(vertical: 6),
@@ -420,9 +422,10 @@ class _EditAccountScreenState extends ConsumerState<EditAccountScreen> {
labelText: label, labelText: label,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
validator: required validator: validator ??
? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null (required
: null, ? (v) => (v == null || v.trim().isEmpty) ? 'Required' : null
: null),
), ),
); );
} }