diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index f9f530ec..4c2d7320 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -190,7 +190,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s | 4721 | Client escaping of record text | F33. **Done** (raven-client: `escapeHtml` helper for the warranty summary; address dialog as text; search excerpt escaped with the terms highlighted as plain text; dialog messages through DOMPurify. The excerpt is not encoded on the server as well: the client escaping covers it. John checked search, warranty info with `` in a vendor name, the address dialog and confirmation dialogs) | | 4722 | DOMPurify and Vuetify upgrades (raven-client) | F34. **Done** (raven-client `44411ec1`: DOMPurify 2.5.9, Vuetify 2.7.2, builds on Node 12.22.9; Vuetify 2.7's calendar `eventSummary()` returns an element, so the six calendar event templates render it with a new `gz-vnodes` component instead of `v-html`. John checked wiki, schedules (drag, stretch, lock icons), date pickers, lists, dialogs and the reminder and review widgets. John also checked the scheduled work order widget as a Tech user. Still to do: Cypress on `devtestn10` at the final gate) | | 4723 | Direct SMTP and direct notification: staff only, record rights | F3, F4. **Done**, plan revised with John: staff keep sending to any address (stock report template and a customer script use it); contacts refused in both actions, an address from a record needs read rights on it, direct notifications refused to subcontractors. Test `DirectSend` (the subcontractor case returned 204 on the old code). No changelog line: nothing that works changes | -| 4724 | SMTP: certificate checked, password never returned | F30, F31 | +| 4724 | SMTP: certificate checked, password never returned | F30, F31. **Done** (certificate checked, opt-out config setting `AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE` with manual page; password masked as `********` and kept on save; test `SmtpPasswordHidden`; self-signed listener proof; changelog lines for both) | | 4725 | Sessions: password change ends sessions, exact-path token exemption, default SuperUser refused when licensed | F7, F8, F9 | | 4726 | Login throttling/lockout | F6 | | 4727 | Low severity hardening | F10, F11, F12, F22, F23, F25, F29, F35, F36, F38; the AyaNova half of F39 (DB ID not shown anonymously) | @@ -250,6 +250,8 @@ FastMail password after the old Sockeye server is retired), John's cases. Found during the port, out of scope. One line each. - `NotifyController.SendNotifySmtpDirectMessage`, User branch: the missing-address and inactive messages say "Vendor" instead of "User" (found in case 4723) +- SMTP connection security mapping (`Mailer.cs`): "StartTls" connects with `useSsl: true`, which MailKit treats as TLS from the start (port 465), not STARTTLS (587); "None" uses STARTTLS when offered, which can be stripped. Correcting it could break customers using "StartTls" on port 465 (found in case 4724) +- Race in the notification system: a record saved while a matching notification subscription is being deleted fails with 500 (`anotifyevent_notifysubscriptionid_fkey` violation), and the user's save fails with it. Seen once in the integration suite (`CustomerCrud.CRUD`, run in parallel with subscription tests) during case 4724 - **Case 4731 (John, priority 1, after 8.3.0):** a global notification setting "allow email to any address from the API and report scripts" for `notify/direct-smtp`; off for new installs, on for upgraded databases; when off, only a record's saved address. Follows from F3 / case 4723 - `License.DoFetchAsync` (`License.cs:629-661`) rethrows at `x == 3`, so the fifth server (Sockeye) is never tried, and it can return `null` to a caller that dereferences it - Stray `raven-client\ayanova\cypress.json`: the Cypress suite lives in `raven-test\e2e` diff --git a/devdocs/modernization/SECURITY-REVIEW.md b/devdocs/modernization/SECURITY-REVIEW.md index dbcaf623..bd348d08 100644 --- a/devdocs/modernization/SECURITY-REVIEW.md +++ b/devdocs/modernization/SECURITY-REVIEW.md @@ -210,8 +210,8 @@ take the next free number; a fixed or accepted finding keeps its ID and changes | F3 | High | **fixed in cases 4712 and 4723** (staff sending to any address kept, by design) | Any logged-in user, customer contacts included, can send arbitrary email through the company's SMTP server | | F23 | Medium | confirmed (code) | Logo upload accepts any content type and the anonymous logo route serves it inline from the app's origin (stored XSS) | | F24 | Medium | **fixed in case 4718** | Translation import has no role check: any login can add translations | -| F30 | Medium | confirmed (code) | SMTP: the mail server's TLS certificate is never checked | -| F31 | Medium | confirmed (code) | The SMTP password is returned to read-only roles (OpsAdminRestricted, BizAdmin, BizAdminRestricted) | +| F30 | Medium | **fixed in case 4724** | SMTP: the mail server's TLS certificate is never checked | +| F31 | Medium | **fixed in case 4724** | The SMTP password is returned to read-only roles (OpsAdminRestricted, BizAdmin, BizAdminRestricted) | | F32 | Medium | confirmed (code) | A real-looking SMTP account and password are in the DEBUG defaults in `raven` and `sockeye` source | | F34 | Medium | **fixed in case 4722** | Shipped client libraries behind on security fixes: DOMPurify 2.3.8 (wiki sanitiser), Vuetify 2.6.6 | | F6 | Medium | confirmed | No lockout or rate limit on login or the 2FA step; no password rules | @@ -647,7 +647,7 @@ so any certificate is accepted. Anyone who can intercept the connection (a hosti DNS) can pose as the mail server and collect the SMTP account and password and every message. Also, with security set to none, MailKit's `ConnectAsync(host, port, false)` uses STARTTLS only if offered, which an attacker can suppress. Fix: remove the callback (validate normally); if a -customer needs a self-signed mail server, make that an explicit setting. +customer needs a self-signed mail server, make that an explicit setting. **Fixed in case 4724 (2026-10-24):** the callback is set only when the new config setting `AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE` is true (boot warning when it is; shown in server info; manual page `ops-config-smtp-accept-any-certificate.md`). Revocation checking is off (`CheckCertificateRevocation = false`) so an unreachable revocation server can't stop email; trust and name are checked. Proved locally against a TLS listener with a self-signed certificate: refused ("root certificate which is not trusted") without the setting, accepted with it. The STARTTLS-stripping point for security None stays as chosen by the customer; the security option mapping is in Deferred. ### F31 SMTP password returned to read-only roles (pass 8, from code) @@ -655,7 +655,7 @@ customer needs a self-signed mail server, make that an explicit setting. to every role with read rights on `OpsNotificationSettings`: OpsAdminRestricted, BizAdmin and BizAdminRestricted, while only OpsAdmin may change it. Those users can then use the company's mail account directly. Fix: never send the password to the client (return a "set" flag; keep the -stored value when the PUT leaves it empty). +stored value when the PUT leaves it empty). **Fixed in case 4724 (2026-10-24):** the GET returns `********` (entity detached first); a PUT with `********` or an empty value keeps the stored password. Test `SmtpPasswordHidden`; the stored password's fingerprint was unchanged after the round trip. Before the fix BizAdmin received the full password. ### F32 SMTP credentials in source (pass 8) diff --git a/docs/8.0/ayanova/docs/changelog.md b/docs/8.0/ayanova/docs/changelog.md index 41d418be..0bf03fdd 100644 --- a/docs/8.0/ayanova/docs/changelog.md +++ b/docs/8.0/ayanova/docs/changelog.md @@ -26,6 +26,8 @@ IN PROGRESS - Server: the automatic internal backup now runs the database backup utility directly instead of through a command shell, so the database password can contain any characters, and a failed backup is now reported as an error instead of appearing to succeed. See [backup](ops-form-backup.md). - Server: log files are now named with an increasing number when they are archived; the [server log](ops-log.md) form lists them newest first. - Server: removed an unused internal performance profiling component. +- Server: AyaNova now checks the email (SMTP) server's security certificate when sending email. If your mail server uses a self-signed or internal certificate and email notifications stop after upgrading, see [SMTP certificate check](ops-config-smtp-accept-any-certificate.md). +- Server: the SMTP server password is no longer shown in the [notification settings](ops-notification-system.md) form; it shows `********` when a password is set. Type a new password to change it. - Linux: Ubuntu 24.04 and 26.04 are now tested and supported. On Ubuntu 24.04 and newer the .NET runtime is installed from Ubuntu's own package archive. - Docs: updated the [Linux server](ops-install-linux-server.md), [Linux desktop](ops-install-linux-desktop.md), [Windows local network](ops-install-windows-lan.md) and [Windows IIS](ops-install-windows-iis.md) installation and upgrade instructions for .NET 10 and Ubuntu 24.04 / 26.04. - Docs: updated the database password guidance in the installation and [backup](ops-form-backup.md) documentation. diff --git a/docs/8.0/ayanova/docs/ops-config-environment-variables.md b/docs/8.0/ayanova/docs/ops-config-environment-variables.md index dd46cdbd..28dd0a0f 100644 --- a/docs/8.0/ayanova/docs/ops-config-environment-variables.md +++ b/docs/8.0/ayanova/docs/ops-config-environment-variables.md @@ -95,6 +95,10 @@ Command line parameters are appended to the command to start the ayanova server, - [AYANOVA_REPORT_RENDERING_TIMEOUT](ops-config-report-rendering-timeout.md) - [AYANOVA_REPORT_RENDER_API_URL_OVERRIDE](ops-config-report-rendering-api-url-override.md) +### EMAIL + +- [AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE](ops-config-smtp-accept-any-certificate.md) + ### SECURITY - [AYANOVA_SET_SUPERUSER_PW](ops-config-set-superuser-pw.md) diff --git a/docs/8.0/ayanova/docs/ops-config-smtp-accept-any-certificate.md b/docs/8.0/ayanova/docs/ops-config-smtp-accept-any-certificate.md new file mode 100644 index 00000000..9c093b95 --- /dev/null +++ b/docs/8.0/ayanova/docs/ops-config-smtp-accept-any-certificate.md @@ -0,0 +1,53 @@ +# SMTP certificate check setting + +When AyaNova connects to the SMTP server set in the [notification settings](ops-notification-system.md) to send email, it checks the mail server's security certificate. The certificate must be issued by a trusted certificate authority and must match the SMTP server address entered in the notification settings. This prevents someone on the network from posing as your mail server to collect the SMTP account password and the messages being sent. + +Most mail servers and email providers have a certificate that passes this check and nothing needs to be done. + +## When the check fails + +If the mail server's certificate can not be validated, email notifications are not sent. The Send test message option in the notification settings shows an error such as "The server's SSL certificate could not be validated", and failed deliveries are reported in the [notification system](ops-notification-system.md) as usual. + +This can happen when: + +- the mail server uses a self-signed certificate or one issued by your organization's own internal certificate authority +- the SMTP server address is entered as an IP address or a name that the certificate does not cover + +The best fix is to correct the cause: enter the SMTP server address exactly as named in its certificate, or install your internal certificate authority as trusted on the computer running the AyaNova server. If neither is possible, the check can be switched off with this setting. + +## Default + +If this setting is not specified the certificate is checked. + +## Overriding + +AyaNova expects this value to be provided by a config.json property, environment variable or command line parameter named + +`AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE` + +Set it to `true` to accept any certificate from the mail server. Only do this for a mail server on a network you trust: without the check, the connection to the mail server is encrypted but AyaNova can not tell if it is talking to your real mail server. + +When it is set, the AyaNova server log records a warning each time the server starts. + +Example config.json entry + +```json +{ + ...other properties... + "AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE": "true" +} +``` + +Example command line parameter + +`ayanova.exe --AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE="true"` + +Example environment variable + +Windows + +`set "AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE=true"` + +Linux + +`export AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE="true"` diff --git a/docs/8.0/ayanova/docs/ops-notification-system.md b/docs/8.0/ayanova/docs/ops-notification-system.md index 2d4afbfc..7b2482f7 100644 --- a/docs/8.0/ayanova/docs/ops-notification-system.md +++ b/docs/8.0/ayanova/docs/ops-notification-system.md @@ -46,7 +46,7 @@ Typically this is an email address, for example `noreply@example.com` but some m #### SMTP server password -The password the SMTP server requires to send email using the SMTP server account address. Note that this password is visible in this form to system operators and the read only roles listed above. +The password the SMTP server requires to send email using the SMTP server account address. Once saved, the password is never shown in this form: it shows `********` when a password is set. To change it, type the new password and save. Leaving `********` in place keeps the saved password. We recommend a dedicated email account used only for sending notifications. @@ -60,6 +60,8 @@ The choices are: - SSL \ TLS recommended and most secure. Implicit forced security. - StartTLS not recommended, negotiated security, may or may not be secure depending on the capabilities of the mail server. +AyaNova checks the mail server's security certificate when it connects. See [SMTP certificate check](ops-config-smtp-accept-any-certificate.md) if the test message reports that the certificate could not be validated. + #### SMTP server port This is the port that must match the SMTP server's mail port. Most modern mail servers use port `587` which is the current recommended standard port for encrypted mail transmission. diff --git a/docs/8.0/ayanova/mkdocs.yml b/docs/8.0/ayanova/mkdocs.yml index 59c25dc9..bd6aad38 100644 --- a/docs/8.0/ayanova/mkdocs.yml +++ b/docs/8.0/ayanova/mkdocs.yml @@ -197,6 +197,7 @@ nav: - "Reporting render browser path": "ops-config-report-render-browser-path.md" - "Reporting timeout configuration": "ops-config-report-rendering-timeout.md" - "Environment variable reference": "ops-config-environment-variables.md" + - "SMTP certificate check": "ops-config-smtp-accept-any-certificate.md" - "Security and JWT web tokens": "ops-config-jwt-secret.md" - "SuperUser password reset": "ops-config-set-superuser-pw.md" - "Server error codes": "ops-error-codes.md" diff --git a/server/AyaNova/Controllers/GlobalOpsNotificationSettingsController.cs b/server/AyaNova/Controllers/GlobalOpsNotificationSettingsController.cs index 66c0264b..9a415440 100644 --- a/server/AyaNova/Controllers/GlobalOpsNotificationSettingsController.cs +++ b/server/AyaNova/Controllers/GlobalOpsNotificationSettingsController.cs @@ -60,6 +60,10 @@ namespace AyaNova.Api.Controllers if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND)); + //case 4724: the SMTP password never goes to the client (detached first so the mask can't be saved) + ct.Entry(o).State = EntityState.Detached; + o.SmtpPassword = GlobalOpsNotificationSettingsBiz.SMTP_PASSWORD_MASK; + return Ok(ApiOkResponse.Response(o)); } diff --git a/server/AyaNova/Controllers/ServerStateController.cs b/server/AyaNova/Controllers/ServerStateController.cs index 5fc338dc..4e39b1ee 100644 --- a/server/AyaNova/Controllers/ServerStateController.cs +++ b/server/AyaNova/Controllers/ServerStateController.cs @@ -193,6 +193,7 @@ namespace AyaNova.Api.Controllers AYANOVA_LOG_PATH = ServerBootConfig.AYANOVA_LOG_PATH, AYANOVA_LOG_LEVEL = ServerBootConfig.AYANOVA_LOG_LEVEL, AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG = ServerBootConfig.AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG, + AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE = ServerBootConfig.AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE, serverinfo = ServerBootConfig.BOOT_DIAGNOSTIC_INFO, dbserverinfo = ServerBootConfig.DBSERVER_DIAGNOSTIC_INFO })); @@ -315,6 +316,7 @@ namespace AyaNova.Api.Controllers sbRet.AppendLine($"AYANOVA_LOG_PATH: {ServerBootConfig.AYANOVA_LOG_PATH}"); sbRet.AppendLine($"AYANOVA_LOG_LEVEL: {ServerBootConfig.AYANOVA_LOG_LEVEL}"); sbRet.AppendLine($"AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG: {ServerBootConfig.AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG}"); + sbRet.AppendLine($"AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE: {ServerBootConfig.AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE}"); sbRet.AppendLine("#########################################################"); sbRet.AppendLine("SERVER DIAGNOSTICS"); diff --git a/server/AyaNova/Program.cs b/server/AyaNova/Program.cs index 5e2acf6f..b5f4f25d 100644 --- a/server/AyaNova/Program.cs +++ b/server/AyaNova/Program.cs @@ -228,6 +228,9 @@ namespace AyaNova if (ServerBootConfig.AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG) logger.Warn("AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG is enabled, this will cause the server to run very slowly"); + if (ServerBootConfig.AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE) + logger.Warn("AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE is enabled, the SMTP server's certificate is not checked"); + //Log environmental settings long UtilityFilesAvailableSpace = 0; diff --git a/server/AyaNova/biz/GlobalOpsNotificationSettingsBiz.cs b/server/AyaNova/biz/GlobalOpsNotificationSettingsBiz.cs index d4091d1b..5570e87a 100644 --- a/server/AyaNova/biz/GlobalOpsNotificationSettingsBiz.cs +++ b/server/AyaNova/biz/GlobalOpsNotificationSettingsBiz.cs @@ -10,6 +10,8 @@ namespace AyaNova.Biz internal class GlobalOpsNotificationSettingsBiz : BizObject { + //case 4724: shown to the client in place of the SMTP password, which never leaves the server + internal const string SMTP_PASSWORD_MASK = "********"; internal GlobalOpsNotificationSettingsBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) { @@ -62,6 +64,9 @@ namespace AyaNova.Biz if (dbObject == null) throw new System.Exception("GlobalOpsNotificationSettings::PutAsync -> Settings object not found in database!!"); + //case 4724: the client gets the mask, not the password; the mask or an empty value keeps the stored password + if (string.IsNullOrEmpty(putObject.SmtpPassword) || putObject.SmtpPassword == SMTP_PASSWORD_MASK) + putObject.SmtpPassword = dbObject.SmtpPassword; CopyObject.Copy(putObject, dbObject, "Id"); ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency; Validate(dbObject); diff --git a/server/AyaNova/util/Mailer.cs b/server/AyaNova/util/Mailer.cs index b9f5d31e..5ca38197 100644 --- a/server/AyaNova/util/Mailer.cs +++ b/server/AyaNova/util/Mailer.cs @@ -83,7 +83,12 @@ namespace AyaNova.Util using (var client = new SmtpClient()) { - client.ServerCertificateValidationCallback = (s, c, h, e) => true; + //case 4724: the server's certificate must be trusted and match the server name, unless the + //AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE setting says otherwise. No revocation check: it fails the + //connection whenever the certificate authority's revocation server can't be reached + client.CheckCertificateRevocation = false; + if (ServerBootConfig.AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE) + client.ServerCertificateValidationCallback = (s, c, h, e) => true; bool UseSSL = smtpSettings.ConnectionSecurity != Biz.NotifyMailSecurity.None; await client.ConnectAsync(smtpSettings.SmtpServerAddress, smtpSettings.SmtpServerPort, UseSSL); diff --git a/server/AyaNova/util/ServerBootConfig.cs b/server/AyaNova/util/ServerBootConfig.cs index 86b7c18a..29a33a41 100644 --- a/server/AyaNova/util/ServerBootConfig.cs +++ b/server/AyaNova/util/ServerBootConfig.cs @@ -108,6 +108,10 @@ namespace AyaNova.Util //SECURITY internal static string AYANOVA_SET_SUPERUSER_PW { get; set; } + //EMAIL + //case 4724: the SMTP server's certificate is checked unless this is set (a mail server with a self-signed or internal certificate) + internal static bool AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE { get; set; } + //HELPFUL INFORMATION FOR DIAGNOSTICS internal static Dictionary BOOT_DIAGNOSTIC_INFO { get; set; } = new Dictionary(); @@ -218,6 +222,10 @@ namespace AyaNova.Util bTemp = config.GetValue("AYANOVA_REMOVE_LICENSE_FROM_DB"); AYANOVA_REMOVE_LICENSE_FROM_DB = (null == bTemp) ? false : (bool)bTemp; + //EMAIL + bTemp = config.GetValue("AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE"); + AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE = (null == bTemp) ? false : (bool)bTemp; + //FOLDERS string DataFolderPath = ActualFullPath(config.GetValue("AYANOVA_DATA_PATH")); string LogPath = ActualFullPath(config.GetValue("AYANOVA_LOG_PATH"));