Skip to content

Commit 11864b6

Browse files
authored
Add URL validation to operational server webhooks (#1887)
1 parent f3942a4 commit 11864b6

File tree

2 files changed

+43
-2
lines changed

2 files changed

+43
-2
lines changed

Diff for: server/svix-server/src/cfg.rs

+33
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ use figment::{
1111
use ipnet::IpNet;
1212
use serde::{Deserialize, Deserializer};
1313
use tracing::Level;
14+
use url::Url;
1415
use validator::{Validate, ValidationError};
1516

1617
use crate::{
1718
core::{cryptography::Encryption, security::JwtSigningConfig},
1819
error::Result,
20+
v1::utils::validation_error,
1921
};
2022

2123
fn deserialize_main_secret<'de, D>(deserializer: D) -> Result<Encryption, D::Error>
@@ -74,6 +76,36 @@ fn default_redis_pending_duration_secs() -> u64 {
7476
45
7577
}
7678

79+
fn validate_operational_webhook_url(url: &str) -> Result<(), ValidationError> {
80+
match Url::parse(url) {
81+
Ok(url) => {
82+
// Verify scheme is http or https
83+
if url.scheme() != "http" && url.scheme() != "https" {
84+
return Err(validation_error(
85+
Some("operational_webhook_address"),
86+
Some("URL scheme must be http or https"),
87+
));
88+
}
89+
90+
// Verify there's a host
91+
if url.host().is_none() {
92+
return Err(validation_error(
93+
Some("operational_webhook_address"),
94+
Some("URL must include a valid host"),
95+
));
96+
}
97+
}
98+
Err(_) => {
99+
return Err(validation_error(
100+
Some("operational_webhook_address"),
101+
Some("Invalid URL format"),
102+
));
103+
}
104+
}
105+
106+
Ok(())
107+
}
108+
77109
#[derive(Clone, Debug, Deserialize, Validate)]
78110
#[validate(
79111
schema(function = "validate_config_complete"),
@@ -85,6 +117,7 @@ pub struct ConfigurationInner {
85117

86118
/// The address to send operational webhooks to. When None, operational webhooks will not be
87119
/// sent. When Some, the API server with the given URL will be used to send operational webhooks.
120+
#[validate(custom = "validate_operational_webhook_url")]
88121
pub operational_webhook_address: Option<String>,
89122

90123
/// The main secret used by Svix. Used for client-side encryption of sensitive data, etc.

Diff for: server/svix-server/src/core/operational_webhooks.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,15 @@ pub struct OperationalWebhookSenderInner {
115115
}
116116

117117
impl OperationalWebhookSenderInner {
118-
pub fn new(keys: Arc<JwtSigningConfig>, url: Option<String>) -> Arc<Self> {
118+
pub fn new(keys: Arc<JwtSigningConfig>, mut url: Option<String>) -> Arc<Self> {
119+
// Sanitize the URL if present
120+
if let Some(url) = &mut url {
121+
// Remove trailing slashes
122+
while url.ends_with('/') {
123+
url.pop();
124+
}
125+
}
126+
119127
Arc::new(Self {
120128
signing_config: keys,
121129
url,
@@ -177,7 +185,7 @@ impl OperationalWebhookSenderInner {
177185
..
178186
})) => {
179187
tracing::warn!(
180-
"Operational webhooks are enabled but no listener set for {}",
188+
"Operational webhooks are enabled, but no listener found for organization {}",
181189
recipient_org_id,
182190
);
183191
}

0 commit comments

Comments
 (0)