Skip to content

Commit 77db100

Browse files
localstack-spiral[bot]spiralsabir-akhadov-localstack
authored
LAV-2704: Internal __snowflake$ SECURITY DEFINER helpers (exec_object_ddl, create_task, …) are callable from user SQL (#3119)
* LAV-2704: reject user references to internal helper schema Add an ObjectName visitor boundary before procedure lowering and all canonical transform passes, returning Snowflake Cloud’s 002141 qualified-routine error before PostgreSQL can execute an internal helper. Document the trust boundary and capture function, procedure, and privilege-escalation regressions against Cloud. Swept AST object-name positions through the generic visitor rather than individual expression/statement variants; relations, routines, types, casts, table functions, and nested visited statements all share the guard. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix qualified function × rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_routines_are_unknown[SELECT] qualified procedure × rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_routines_are_unknown[CALL] SECURITY DEFINER mutation × rejection/absence -> tests/queries/test_internal_schema_guard.py::test_privileged_ddl_helper_cannot_create_objects transform-introduced helpers × trusted path -> existing compat DDL/SHOW/DESC/RESULT_SCAN/table-procedure suites executor-owned privileged DDL × trusted path -> existing function/procedure compat suites * LAV-2704: enforce the user AST boundary across re-entry paths Move the origin guard ahead of server rewrites, retain a trusted top-level transform entry for executor-generated AST, and reapply the guarded entry to stored and dynamic SQL. Preserve Cloud-specific routine, type, and relation error shapes. Migrate every waivered wire dependency: public equivalents cover NULL/ANY behavior, while account, task, data-metric, and stage implementation probes use the PostgreSQL test-infrastructure route. Swept the user-AST surface for the object-name escape pattern; explicitly covered function expressions, CALL/bare CALL, relations, casts/types, stored SQL bodies, literal dynamic SQL, and generic object-name positions; trusted server/result-scan and transform-generated paths remain outside the rejection pass. test_edit_waiver: tests/queries/ddl/test_account_edition.py tests/queries/functions/test_array_functions.py tests/queries/functions/test_data_metric_functions.py tests/queries/functions/test_date_functions.py tests/queries/functions/test_decfloat_aggregates.py tests/queries/functions/test_overload_gate.py tests/queries/stages/test_stages.py tests/queries/test_any_casts.py tests/queries/test_any_comparison.py tests/queries/test_any_dispatch.py tests/queries/test_any_opclass.py tests/queries/test_null_comparison.py tests/queries/test_tasks.py Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix qualified function x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_routines_are_unknown qualified procedure x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_routines_are_unknown quoted/case-varied qualification x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_routines_are_unknown internal type x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_unknown_in_every_user_object_position internal relation x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_unknown_in_every_user_object_position stored SQL body x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_rejected_in_stored_sql_body dynamic SQL x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_unknown_in_every_user_object_position SECURITY DEFINER mutation x rejection/absence -> tests/queries/test_internal_schema_guard.py::test_privileged_ddl_helper_cannot_create_objects public function lifecycle x trusted path -> tests/queries/test_internal_schema_guard.py::test_trusted_internal_paths_remain_usable public procedure lifecycle x trusted path -> tests/queries/test_internal_schema_guard.py::test_trusted_internal_paths_remain_usable store-writing DDL x trusted path -> tests/queries/test_internal_schema_guard.py::test_trusted_internal_paths_remain_usable SHOW/DESC x trusted path -> tests/queries/test_internal_schema_guard.py::test_trusted_internal_paths_remain_usable RESULT_SCAN x trusted path -> tests/queries/test_internal_schema_guard.py::test_trusted_internal_paths_remain_usable table procedure x trusted path -> tests/queries/test_internal_schema_guard.py::test_trusted_internal_paths_remain_usable implementation-only probes x infrastructure path -> tests/conftest.py::pg_execute * LAV-2704: record compat test edit waivers Record the spec-granted paths individually for the repository guard, whose waiver parser treats the spec's space-separated line as one path. test_edit_waiver: tests/queries/ddl/test_account_edition.py test_edit_waiver: tests/queries/functions/test_array_functions.py test_edit_waiver: tests/queries/functions/test_data_metric_functions.py test_edit_waiver: tests/queries/functions/test_date_functions.py test_edit_waiver: tests/queries/functions/test_decfloat_aggregates.py test_edit_waiver: tests/queries/functions/test_overload_gate.py test_edit_waiver: tests/queries/stages/test_stages.py test_edit_waiver: tests/queries/test_any_casts.py test_edit_waiver: tests/queries/test_any_comparison.py test_edit_waiver: tests/queries/test_any_dispatch.py test_edit_waiver: tests/queries/test_any_opclass.py test_edit_waiver: tests/queries/test_null_comparison.py test_edit_waiver: tests/queries/test_tasks.py Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix qualified function/procedure and case variants x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_routines_are_unknown internal object/type and dynamic SQL x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_unknown_in_every_user_object_position stored SQL body x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_rejected_in_stored_sql_body SECURITY DEFINER mutation x rejection/absence -> tests/queries/test_internal_schema_guard.py::test_privileged_ddl_helper_cannot_create_objects public DDL and transformed paths x trusted execution -> tests/queries/test_internal_schema_guard.py::test_trusted_internal_paths_remain_usable implementation-only probes x infrastructure path -> tests/conftest.py::pg_execute * LAV-2704: guard opaque SQL function bodies Apply the user-origin internal-schema guard immediately after reparsing opaque SQL UDF bodies, before transform_inner can introduce trusted identifiers. Cover scalar expression and statement bodies, scripting UDF blocks, and data metric function bodies. Swept procedure_transformer and sibling data-metric lowering for the opaque-body reparse pattern; guarded every user-authored parsed AST before inner transformation; no other parse-to-transform_inner sibling remains. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix scalar SQL UDF statement body x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_rejected_in_stored_sql_body scalar SQL UDF bare-expression body x rejection -> guarded by the same transform_function_body branch; uncovered: parser fallback shares the identical guard and error path scripting SQL UDF body x rejection -> guarded before scripting lowering; uncovered: same AST visitor and error path SQL data metric body x rejection -> guarded before data-metric lowering; uncovered: same AST visitor and error path trusted public function/procedure lifecycle x success -> tests/queries/test_internal_schema_guard.py::test_trusted_internal_paths_remain_usable * LAV-2704: reject opaque internal-schema bodies Reject case-varied internal-schema references when neither direct nor SELECT-wrapped SQL function body parsing succeeds, instead of forwarding the opaque body unchanged to PostgreSQL. Repurpose the stored-body regression to exercise this parse-failure fallback while matching the existing Cloud-captured 002141 error shape. Swept transform_function_body for all parse exits; both successfully parsed branches use the AST guard, and the sole unparsed forwarding branch now applies the raw-text rejection required by the human decision. Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix parsed SQL UDF body x rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_unknown_in_every_user_object_position opaque parse-failure SQL UDF body x case-varied rejection -> tests/queries/test_internal_schema_guard.py::test_internal_schema_is_rejected_in_stored_sql_body opaque parse-failure body without internal schema x unchanged forwarding -> existing SQL UDF compat coverage * LAV-2704: format merged guard imports Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> * LAV-2704: repair guarded compat infrastructure Correct the PostgreSQL CSV alias used by account-edition inspection, keep NULL/ANY implementation probes outside the user transform boundary, and use public Snowflake expressions for the remaining parity cases. Swept the failed changed-file compat set for the same wire-internal-type pattern; ARRAY_SLICE, date-part, and error-only ANY dispatch probes now use pg_execute, while all other ANY cases use public SQL. test_edit_waiver: tests/queries/ddl/test_account_edition.py tests/queries/functions/test_array_functions.py tests/queries/functions/test_data_metric_functions.py tests/queries/functions/test_date_functions.py tests/queries/functions/test_decfloat_aggregates.py tests/queries/functions/test_overload_gate.py tests/queries/stages/test_stages.py tests/queries/test_any_casts.py tests/queries/test_any_comparison.py tests/queries/test_any_dispatch.py tests/queries/test_any_opclass.py tests/queries/test_null_comparison.py tests/queries/test_tasks.py Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud> ## Test matrix account edition infrastructure read x success -> tests/queries/ddl/test_account_edition.py::test_create_account_persists_edition_and_is_reachable ARRAY_SLICE snowflake_null overloads x NULL -> tests/queries/functions/test_array_functions.py::test_array_slice_snowflake_null_overloads date-part snowflake_null overloads x NULL -> tests/queries/functions/test_date_functions.py::test_year_month_day_snowflake_null_overloads ANY public input types x parity -> tests/queries/test_any_dispatch.py ANY error-only dispatch x exact error -> tests/queries/test_any_dispatch.py::test_any_encrypt_raw_single_arg_errors and sibling error tests --------- Co-authored-by: spiral <spiral@localhost> Co-authored-by: Sabir Akhadov <sabir.akhadov@localstack.cloud>
1 parent 6e94bec commit 77db100

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

‎src/ast/mod.rs‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,26 @@ impl VisitMut for Ident {
422422
/// A name of a table, view, custom type, etc., possibly multi-part, i.e. db.schema.obj
423423
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
424424
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
425-
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
426425
pub struct ObjectName(pub Vec<ObjectNamePart>);
427426

427+
#[cfg(feature = "visitor")]
428+
impl Visit for ObjectName {
429+
fn visit<V: Visitor>(&self, visitor: &mut V) -> ControlFlow<V::Break> {
430+
visitor.pre_visit_object_name(self)?;
431+
Visit::visit(&self.0, visitor)?;
432+
visitor.post_visit_object_name(self)
433+
}
434+
}
435+
436+
#[cfg(feature = "visitor")]
437+
impl VisitMut for ObjectName {
438+
fn visit<V: VisitorMut>(&mut self, visitor: &mut V) -> ControlFlow<V::Break> {
439+
visitor.pre_visit_object_name(self)?;
440+
VisitMut::visit(&mut self.0, visitor)?;
441+
visitor.post_visit_object_name(self)
442+
}
443+
}
444+
428445
impl From<Vec<Ident>> for ObjectName {
429446
fn from(idents: Vec<Ident>) -> Self {
430447
ObjectName(idents.into_iter().map(ObjectNamePart::Identifier).collect())

‎src/ast/visitor.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,16 @@ pub trait Visitor {
197197
/// boxing it with `Box` to minimize stack usage.
198198
type Break;
199199

200+
/// Invoked for any object name before visiting its parts.
201+
fn pre_visit_object_name(&mut self, _name: &ObjectName) -> ControlFlow<Self::Break> {
202+
ControlFlow::Continue(())
203+
}
204+
205+
/// Invoked for any object name after visiting its parts.
206+
fn post_visit_object_name(&mut self, _name: &ObjectName) -> ControlFlow<Self::Break> {
207+
ControlFlow::Continue(())
208+
}
209+
200210
/// Invoked for any queries that appear in the AST before visiting children
201211
fn pre_visit_query(&mut self, _query: &Query) -> ControlFlow<Self::Break> {
202212
ControlFlow::Continue(())
@@ -329,6 +339,16 @@ pub trait VisitorMut {
329339
/// boxing it with `Box` to minimize stack usage.
330340
type Break;
331341

342+
/// Invoked for any object name before visiting its parts.
343+
fn pre_visit_object_name(&mut self, _name: &mut ObjectName) -> ControlFlow<Self::Break> {
344+
ControlFlow::Continue(())
345+
}
346+
347+
/// Invoked for any object name after visiting its parts.
348+
fn post_visit_object_name(&mut self, _name: &mut ObjectName) -> ControlFlow<Self::Break> {
349+
ControlFlow::Continue(())
350+
}
351+
332352
/// Invoked for any queries that appear in the AST before visiting children
333353
fn pre_visit_query(&mut self, _query: &mut Query) -> ControlFlow<Self::Break> {
334354
ControlFlow::Continue(())

0 commit comments

Comments
 (0)