summaryrefslogtreecommitdiff
path: root/DomainDig
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-20 12:05:18 -0500
committerChristian Cleberg <[email protected]>2026-07-20 13:56:41 -0500
commit6f8a441b622987ffe102cf7153e1bcdd1b730cfc (patch)
tree02c9c0b5f09ff97e8ad7f7c1704584b5333b8aaf /DomainDig
parent77a42a8b4f3dd89e631f8246bdea3e95f83bac6a (diff)
downloaddomain-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')
-rw-r--r--DomainDig/IntegrationService.swift19
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"