diff options
| author | Christian Cleberg <[email protected]> | 2026-07-20 12:05:18 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-20 13:56:41 -0500 |
| commit | 6f8a441b622987ffe102cf7153e1bcdd1b730cfc (patch) | |
| tree | 02c9c0b5f09ff97e8ad7f7c1704584b5333b8aaf /DomainDig/IntegrationService.swift | |
| parent | 77a42a8b4f3dd89e631f8246bdea3e95f83bac6a (diff) | |
| download | domain-dig-6f8a441b622987ffe102cf7153e1bcdd1b730cfc.tar.gz domain-dig-6f8a441b622987ffe102cf7153e1bcdd1b730cfc.tar.bz2 domain-dig-6f8a441b622987ffe102cf7153e1bcdd1b730cfc.zip | |
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.
Diffstat (limited to 'DomainDig/IntegrationService.swift')
| -rw-r--r-- | DomainDig/IntegrationService.swift | 19 |
1 files changed, 18 insertions, 1 deletions
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" |
