From 8e20900a9270a94cb9c3f75a80ab1ee0f15b60be Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Thu, 15 Oct 2026 14:58:03 +0530 Subject: [PATCH] fix: create/update sql filters currently failing because UI sends `stream_name=sql` in create/update filters when called from sql editor the stream name validation was added in the PR https://github.com/parseablehq/parseable/pull/1727 for a security fix https://github.com/parseablehq/parseable/security/advisories/GHSA-q8xx-r33r-6mwv with this change, the security fix still works also, UI will be able to save the filter as-is --- src/handlers/http/users/filters.rs | 41 ++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/handlers/http/users/filters.rs b/src/handlers/http/users/filters.rs index c7a4d5681..85cf0d92e 100644 --- a/src/handlers/http/users/filters.rs +++ b/src/handlers/http/users/filters.rs @@ -22,7 +22,7 @@ use crate::{ parseable::PARSEABLE, rbac::{Users, map::SessionKey}, storage::{ObjectStorageError, StreamType}, - users::filters::{CURRENT_FILTER_VERSION, FILTERS, Filter}, + users::filters::{CURRENT_FILTER_VERSION, FILTERS, Filter, FilterType}, utils::{ actix::extract_session_key_from_req, get_hash, get_user_and_tenant_from_request, is_admin, is_admin_for_session, @@ -38,6 +38,8 @@ use actix_web::{ use serde_json::Error as SerdeError; use ulid::Ulid; +const SQL_FILTER_STREAM_NAME: &str = "sql"; + pub async fn list(req: HttpRequest) -> Result { let key = extract_session_key_from_req(&req).map_err(|e| FiltersError::Custom(e.to_string()))?; @@ -82,7 +84,7 @@ pub async fn post( req: HttpRequest, Json(mut filter): Json, ) -> Result { - validate_stream_name(&filter.stream_name)?; + validate_stream_name(&mut filter.stream_name, &filter.query.filter_type)?; let (mut user_id, tenant_id) = get_user_and_tenant_from_request(&req)?; user_id = get_hash(&user_id); @@ -102,7 +104,7 @@ pub async fn update( filter_id: Path, Json(mut filter): Json, ) -> Result { - validate_stream_name(&filter.stream_name)?; + validate_stream_name(&mut filter.stream_name, &filter.query.filter_type)?; let (mut user_id, tenant_id) = get_user_and_tenant_from_request(&req)?; user_id = get_hash(&user_id); @@ -153,7 +155,15 @@ pub async fn delete( Ok(HttpResponse::Ok().finish()) } -fn validate_stream_name(stream_name: &str) -> Result<(), FiltersError> { +fn validate_stream_name( + stream_name: &mut String, + filter_type: &FilterType, +) -> Result<(), FiltersError> { + if matches!(filter_type, FilterType::SQL) { + SQL_FILTER_STREAM_NAME.clone_into(stream_name); + return Ok(()); + } + validator::stream_name(stream_name, StreamType::UserDefined) .map_err(FiltersError::InvalidStreamName) } @@ -205,20 +215,31 @@ impl actix_web::ResponseError for FiltersError { #[cfg(test)] mod tests { - use super::validate_stream_name; + use super::{SQL_FILTER_STREAM_NAME, validate_stream_name}; + use crate::users::filters::FilterType; use crate::validator::error::StreamNameValidationError; #[test] fn accepts_flat_stream_names() { - assert!(validate_stream_name("team1").is_ok()); - assert!(validate_stream_name("orders-prod").is_ok()); - assert!(validate_stream_name("user_events_v2").is_ok()); + for stream_name in ["team1", "orders-prod", "user_events_v2"] { + let mut stream_name = stream_name.to_owned(); + assert!(validate_stream_name(&mut stream_name, &FilterType::Filter).is_ok()); + } + } + + #[test] + fn normalizes_sql_filter_stream_name() { + let mut stream_name = "../../unsafe-path".to_owned(); + + assert!(validate_stream_name(&mut stream_name, &FilterType::SQL).is_ok()); + assert_eq!(stream_name, SQL_FILTER_STREAM_NAME); } #[test] fn rejects_path_like_stream_names() { - let err = - validate_stream_name("team1/serviceA").expect_err("path-like names must be rejected"); + let mut stream_name = "team1/serviceA".to_owned(); + let err = validate_stream_name(&mut stream_name, &FilterType::Search) + .expect_err("path-like names must be rejected"); assert!(matches!( err, super::FiltersError::InvalidStreamName(StreamNameValidationError::NameSpecialChar {