Add URL and header validity checks

This commit is contained in:
Mubelotix
2025-08-04 16:26:20 +02:00
parent 69c59d3de3
commit 1754745c42
2 changed files with 94 additions and 8 deletions

View File

@ -1,5 +1,9 @@
use std::collections::BTreeMap;
use actix_http::header::{
HeaderName, HeaderValue, InvalidHeaderName as ActixInvalidHeaderName,
InvalidHeaderValue as ActixInvalidHeaderValue,
};
use actix_web::web::{self, Data, Path};
use actix_web::{HttpRequest, HttpResponse};
use deserr::actix_web::AwebJson;
@ -13,6 +17,7 @@ use meilisearch_types::milli::update::Setting;
use meilisearch_types::webhooks::Webhook;
use serde::Serialize;
use tracing::debug;
use url::Url;
use utoipa::{OpenApi, ToSchema};
use uuid::Uuid;
@ -184,6 +189,12 @@ enum WebhooksError {
ReservedWebhook(Uuid),
#[error("Webhook `{0}` not found.")]
WebhookNotFound(Uuid),
#[error("Invalid header name `{0}`: {1}")]
InvalidHeaderName(String, ActixInvalidHeaderName),
#[error("Invalid header value `{0}`: {1}")]
InvalidHeaderValue(String, ActixInvalidHeaderValue),
#[error("Invalid URL `{0}`: {1}")]
InvalidUrl(String, url::ParseError),
}
impl ErrorCode for WebhooksError {
@ -194,6 +205,9 @@ impl ErrorCode for WebhooksError {
TooManyHeaders(_) => meilisearch_types::error::Code::InvalidWebhooksHeaders,
ReservedWebhook(_) => meilisearch_types::error::Code::ReservedWebhook,
WebhookNotFound(_) => meilisearch_types::error::Code::WebhookNotFound,
InvalidHeaderName(_, _) => meilisearch_types::error::Code::InvalidWebhooksHeaders,
InvalidHeaderValue(_, _) => meilisearch_types::error::Code::InvalidWebhooksHeaders,
InvalidUrl(_, _) => meilisearch_types::error::Code::InvalidWebhooksUrl,
}
}
}
@ -239,6 +253,32 @@ fn patch_webhook_inner(
Ok(Webhook { url, headers })
}
fn check_changed(uuid: Uuid, webhook: &Webhook) -> Result<(), WebhooksError> {
if uuid.is_nil() {
return Err(ReservedWebhook(uuid));
}
if webhook.url.is_empty() {
return Err(MissingUrl(uuid));
}
if webhook.headers.len() > 200 {
return Err(TooManyHeaders(uuid));
}
for (header, value) in &webhook.headers {
HeaderName::from_bytes(header.as_bytes())
.map_err(|e| InvalidHeaderName(header.to_owned(), e))?;
HeaderValue::from_str(value).map_err(|e| InvalidHeaderValue(header.to_owned(), e))?;
}
if let Err(e) = Url::parse(&webhook.url) {
return Err(InvalidUrl(webhook.url.to_owned(), e));
}
Ok(())
}
#[utoipa::path(
get,
path = "/{uuid}",
@ -320,6 +360,8 @@ async fn post_webhook(
.map(|h| h.into_iter().map(|(k, v)| (k, v.set().unwrap_or_default())).collect())
.unwrap_or_default(),
};
check_changed(uuid, &webhook)?;
webhooks.webhooks.insert(uuid, webhook.clone());
index_scheduler.put_webhooks(webhooks)?;
@ -363,18 +405,11 @@ async fn patch_webhook(
let webhook_settings = webhook_settings.into_inner();
debug!(parameters = ?(uuid, &webhook_settings), "Patch webhook");
if uuid.is_nil() {
return Err(ReservedWebhook(uuid).into());
}
let mut webhooks = index_scheduler.webhooks();
let old_webhook = webhooks.webhooks.remove(&uuid);
let webhook = patch_webhook_inner(&uuid, old_webhook, webhook_settings)?;
if webhook.headers.len() > 200 {
return Err(TooManyHeaders(uuid).into());
}
check_changed(uuid, &webhook)?;
webhooks.webhooks.insert(uuid, webhook.clone());
index_scheduler.put_webhooks(webhooks)?;

View File

@ -469,3 +469,54 @@ async fn patch() {
}
"#);
}
#[actix_web::test]
async fn invalid_url_and_headers() {
let server = Server::new().await;
// Test invalid URL format
let (value, code) = server.create_webhook(json!({ "url": "not-a-valid-url" })).await;
snapshot!(code, @"400 Bad Request");
snapshot!(value, @r#"
{
"message": "Invalid URL `not-a-valid-url`: relative URL without a base",
"code": "invalid_webhooks_url",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_webhooks_url"
}
"#);
// Test invalid header name (containing spaces)
let (value, code) = server
.create_webhook(json!({
"url": "https://example.com/hook",
"headers": { "invalid header name": "value" }
}))
.await;
snapshot!(code, @"400 Bad Request");
snapshot!(value, @r#"
{
"message": "Invalid header name `invalid header name`: invalid HTTP header name",
"code": "invalid_webhooks_headers",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_webhooks_headers"
}
"#);
// Test invalid header value (containing control characters)
let (value, code) = server
.create_webhook(json!({
"url": "https://example.com/hook",
"headers": { "authorization": "token\nwith\nnewlines" }
}))
.await;
snapshot!(code, @"400 Bad Request");
snapshot!(value, @r#"
{
"message": "Invalid header value `authorization`: failed to parse header value",
"code": "invalid_webhooks_headers",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_webhooks_headers"
}
"#);
}