Skip to content

Commit adead78

Browse files
committed
hardening: parameterize internal SPI lookups
1 parent 23ee3bb commit adead78

2 files changed

Lines changed: 61 additions & 63 deletions

File tree

src/dsl.rs

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,11 @@ pub fn setvar(name: &str, value: &str) -> String {
5858
pgrx::error!("df.setvar() cannot be called inside a workflow - set variables before starting the workflow");
5959
}
6060

61-
let sql = format!(
62-
"INSERT INTO df.vars (name, value) VALUES ('{}', '{}')
61+
if let Err(e) = Spi::run_with_args(
62+
"INSERT INTO df.vars (name, value) VALUES ($1, $2)
6363
ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value",
64-
name.replace('\'', "''"),
65-
value.replace('\'', "''")
66-
);
67-
if let Err(e) = Spi::run(&sql) {
64+
&[name.into(), value.into()],
65+
) {
6866
pgrx::error!("Failed to set variable: {:?}", e);
6967
}
7068
"OK".to_string()
@@ -73,11 +71,12 @@ pub fn setvar(name: &str, value: &str) -> String {
7371
/// Gets a workflow variable value.
7472
#[pg_extern(schema = "df")]
7573
pub fn getvar(name: &str) -> Option<String> {
76-
let sql = format!(
77-
"SELECT value FROM df.vars WHERE name = '{}'",
78-
name.replace('\'', "''")
79-
);
80-
Spi::get_one::<String>(&sql).ok().flatten()
74+
Spi::get_one_with_args::<String>(
75+
"SELECT value FROM df.vars WHERE name = $1",
76+
&[name.into()],
77+
)
78+
.ok()
79+
.flatten()
8180
}
8281

8382
/// Removes a workflow variable.
@@ -88,11 +87,10 @@ pub fn unsetvar(name: &str) -> String {
8887
pgrx::error!("df.unsetvar() cannot be called inside a workflow - manage variables before starting the workflow");
8988
}
9089

91-
let sql = format!(
92-
"DELETE FROM df.vars WHERE name = '{}'",
93-
name.replace('\'', "''")
94-
);
95-
if let Err(e) = Spi::run(&sql) {
90+
if let Err(e) = Spi::run_with_args(
91+
"DELETE FROM df.vars WHERE name = $1",
92+
&[name.into()],
93+
) {
9694
pgrx::error!("Failed to unset variable: {:?}", e);
9795
}
9896
"OK".to_string()
@@ -464,10 +462,10 @@ pub fn signal(instance_id: &str, signal_name: &str, signal_data: default!(&str,
464462

465463
// Ownership check: SPI goes through RLS, so this returns false for
466464
// non-owned instances (the row is invisible to the calling user).
467-
let exists: bool = Spi::get_one(&format!(
468-
"SELECT EXISTS(SELECT 1 FROM df.instances WHERE id = '{}')",
469-
instance_id.replace('\'', "''")
470-
))
465+
let exists: bool = Spi::get_one_with_args(
466+
"SELECT EXISTS(SELECT 1 FROM df.instances WHERE id = $1)",
467+
&[instance_id.into()],
468+
)
471469
.ok()
472470
.flatten()
473471
.unwrap_or(false);
@@ -508,10 +506,10 @@ pub fn start(
508506

509507
// Validate that the target database exists (if specified)
510508
if let Some(db) = database {
511-
let exists: bool = match Spi::get_one(&format!(
512-
"SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = '{}')",
513-
db.replace('\'', "''")
514-
)) {
509+
let exists: bool = match Spi::get_one_with_args(
510+
"SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)",
511+
&[db.into()],
512+
) {
515513
Ok(Some(v)) => v,
516514
Ok(None) => false,
517515
Err(e) => pgrx::error!("failed to check database existence: {}", e),
@@ -684,10 +682,10 @@ pub fn cancel(instance_id: &str, reason: default!(&str, "'Cancelled by user'"))
684682

685683
// Ownership check: SPI goes through RLS, so this returns false for
686684
// non-owned instances (the row is invisible to the calling user).
687-
let exists: bool = Spi::get_one(&format!(
688-
"SELECT EXISTS(SELECT 1 FROM df.instances WHERE id = '{}')",
689-
instance_id.replace('\'', "''")
690-
))
685+
let exists: bool = Spi::get_one_with_args(
686+
"SELECT EXISTS(SELECT 1 FROM df.instances WHERE id = $1)",
687+
&[instance_id.into()],
688+
)
691689
.ok()
692690
.flatten()
693691
.unwrap_or(false);
@@ -701,10 +699,10 @@ pub fn cancel(instance_id: &str, reason: default!(&str, "'Cancelled by user'"))
701699

702700
// Update the instance status to 'cancelled' via SPI.
703701
// User has column-level UPDATE on (status, updated_at) with RLS restricting to own rows.
704-
Spi::run(&format!(
705-
"UPDATE df.instances SET status = 'cancelled', updated_at = now() WHERE id = '{}'",
706-
instance_id.replace('\'', "''")
707-
))
702+
Spi::run_with_args(
703+
"UPDATE df.instances SET status = 'cancelled', updated_at = now() WHERE id = $1",
704+
&[instance_id.into()],
705+
)
708706
.unwrap_or_else(|e| warning!("Failed to update instance status: {e}"));
709707

710708
format!("Instance {instance_id} cancelled: {reason}")
@@ -713,11 +711,12 @@ pub fn cancel(instance_id: &str, reason: default!(&str, "'Cancelled by user'"))
713711
/// Gets the status of a durable function instance.
714712
#[pg_extern(schema = "df")]
715713
pub fn status(instance_id: &str) -> Option<String> {
716-
let sql = format!(
717-
"SELECT status FROM df.instances WHERE id = '{}'",
718-
instance_id.replace('\'', "''")
719-
);
720-
Spi::get_one::<String>(&sql).ok().flatten()
714+
Spi::get_one_with_args::<String>(
715+
"SELECT status FROM df.instances WHERE id = $1",
716+
&[instance_id.into()],
717+
)
718+
.ok()
719+
.flatten()
721720
}
722721

723722
/// Manually runs pending durable functions.
@@ -733,13 +732,14 @@ pub fn run(instance_id: default!(Option<&str>, "NULL")) -> String {
733732
/// Gets the result of a completed durable function.
734733
#[pg_extern(schema = "df")]
735734
pub fn result(instance_id: &str) -> Option<String> {
736-
let escaped_instance_id = instance_id.replace('\'', "''");
737-
let sql = format!(
738-
r#"SELECT result::text FROM df.nodes
739-
WHERE id = (SELECT root_node FROM df.instances WHERE id = '{escaped_instance_id}')
740-
AND status = 'completed'"#
741-
);
742-
Spi::get_one::<String>(&sql).ok().flatten()
735+
Spi::get_one_with_args::<String>(
736+
r#"SELECT result::text FROM df.nodes
737+
WHERE id = (SELECT root_node FROM df.instances WHERE id = $1)
738+
AND status = 'completed'"#,
739+
&[instance_id.into()],
740+
)
741+
.ok()
742+
.flatten()
743743
}
744744

745745
/// Waits for a durable function to complete, returning its final status.
@@ -772,13 +772,11 @@ pub fn wait_for_completion(
772772

773773
loop {
774774
// Query instance status
775-
let sql = format!(
776-
"SELECT status FROM df.instances WHERE id = '{}'",
777-
instance_id.replace('\'', "''")
778-
);
779-
780-
let status: Option<String> =
781-
Spi::get_one(&sql).map_err(|e| format!("Failed to query status: {:?}", e))?;
775+
let status: Option<String> = Spi::get_one_with_args(
776+
"SELECT status FROM df.instances WHERE id = $1",
777+
&[instance_id.into()],
778+
)
779+
.map_err(|e| format!("Failed to query status: {:?}", e))?;
782780

783781
if let Some(ref s) = status {
784782
let s_lower = s.to_lowercase();

src/monitoring.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,18 @@ pub fn instance_info(
125125
let instance_id_str = instance_id.to_string();
126126

127127
// Ownership check: SPI goes through RLS, returning NULL for non-owned instances.
128-
let label: Option<String> = Spi::get_one(&format!(
129-
"SELECT label FROM df.instances WHERE id = '{}'",
130-
instance_id.replace('\'', "''")
131-
))
128+
let label: Option<String> = Spi::get_one_with_args(
129+
"SELECT label FROM df.instances WHERE id = $1",
130+
&[instance_id.into()],
131+
)
132132
.ok()
133133
.flatten();
134134

135135
// Check if the instance exists for this user (RLS-filtered)
136-
let exists: bool = Spi::get_one(&format!(
137-
"SELECT EXISTS(SELECT 1 FROM df.instances WHERE id = '{}')",
138-
instance_id.replace('\'', "''")
139-
))
136+
let exists: bool = Spi::get_one_with_args(
137+
"SELECT EXISTS(SELECT 1 FROM df.instances WHERE id = $1)",
138+
&[instance_id.into()],
139+
)
140140
.ok()
141141
.flatten()
142142
.unwrap_or(false);
@@ -199,10 +199,10 @@ pub fn instance_executions(
199199
let instance_id_owned = instance_id.to_string();
200200

201201
// Ownership check: SPI goes through RLS, so non-owned instances are invisible.
202-
let exists: bool = Spi::get_one(&format!(
203-
"SELECT EXISTS(SELECT 1 FROM df.instances WHERE id = '{}')",
204-
instance_id.replace('\'', "''")
205-
))
202+
let exists: bool = Spi::get_one_with_args(
203+
"SELECT EXISTS(SELECT 1 FROM df.instances WHERE id = $1)",
204+
&[instance_id.into()],
205+
)
206206
.ok()
207207
.flatten()
208208
.unwrap_or(false);

0 commit comments

Comments
 (0)