From 6f8a441b622987ffe102cf7153e1bcdd1b730cfc Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 20 Jul 2026 12:05:18 -0500 Subject: fix: reject non-HTTPS webhook URLs at save time Validating only at send time meant an http:// URL saved fine and then failed silently on delivery. Validate in upsert so the integration editor surfaces it, and give the failure its own error case rather than reusing the generic invalid-URL message. The send-time guard stays as defense in depth for URLs saved before this. --- DomainDig/IntegrationService.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'DomainDig') diff --git a/DomainDig/IntegrationService.swift b/DomainDig/IntegrationService.swift index 9948441..9c9541c 100644 --- a/DomainDig/IntegrationService.swift +++ b/DomainDig/IntegrationService.swift @@ -40,6 +40,7 @@ final class IntegrationService { switch updatedTarget.configuration { case .webhook(var configuration): if let webhookURL { + try Self.validateHTTPS(webhookURL) let reference = configuration.credentialReference ?? Self.secretReference(for: updatedTarget.id, suffix: "webhook") try IntegrationSecretStore.save(secret: webhookURL, reference: reference) configuration.credentialReference = reference @@ -48,6 +49,7 @@ final class IntegrationService { } case .slack(var configuration): if let slackWebhookURL { + try Self.validateHTTPS(slackWebhookURL) let reference = configuration.credentialReference ?? Self.secretReference(for: updatedTarget.id, suffix: "slack") try IntegrationSecretStore.save(secret: slackWebhookURL, reference: reference) configuration.credentialReference = reference @@ -429,6 +431,15 @@ final class IntegrationService { "integration.\(integrationID.uuidString).\(suffix)" } + private static func validateHTTPS(_ string: String) throws { + guard let url = URL(string: string) else { + throw IntegrationError.invalidURL + } + guard url.scheme?.lowercased() == "https" else { + throw IntegrationError.insecureURL + } + } + private static func loadTargets(defaults: UserDefaults) -> [IntegrationTarget] { load([IntegrationTarget].self, key: StorageKey.targets, defaults: defaults) ?? [] } @@ -524,6 +535,7 @@ private struct SlackText: Encodable { private enum IntegrationError: LocalizedError { case invalidURL + case insecureURL case missingSecret case invalidResponse(Int) case invalidSMTPPort @@ -534,6 +546,8 @@ private enum IntegrationError: LocalizedError { switch self { case .invalidURL: return "The integration URL is invalid." + case .insecureURL: + return "The integration URL must use https. A webhook URL is itself a secret, so http would send it in cleartext." case .missingSecret: return "This integration is missing a saved secret." case .invalidResponse(let statusCode): @@ -555,9 +569,12 @@ private enum HTTPIntegrationClient { headers: [String: String], timeoutSeconds: Double ) async throws { - guard let url = URL(string: urlString), url.scheme?.lowercased() == "https" else { + guard let url = URL(string: urlString) else { throw IntegrationError.invalidURL } + guard url.scheme?.lowercased() == "https" else { + throw IntegrationError.insecureURL + } var request = URLRequest(url: url, timeoutInterval: timeoutSeconds) request.httpMethod = "POST" -- cgit v1.2.3