use sparrowdb::open;
use sparrowdb_execution::types::Value;
use std::collections::HashMap;
fn make_db() -> (tempfile::TempDir, sparrowdb::GraphDb) {
let dir = tempfile::tempdir().expect("tempdir");
let db = open(dir.path()).expect("open");
(dir, db)
}
fn params(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
#[test]
fn injection_payload_stored_as_literal_no_role_property_created() {
let (_dir, db) = make_db();
let evil = "\", role: \"admin";
db.execute_with_params(
"CREATE (:User {name: $name})",
params(&[("name", Value::String(evil.to_string()))]),
)
.expect("parameterized CREATE with attacker string must succeed, not inject");
let rows = db
.execute("MATCH (n:User) RETURN n.name")
.expect("MATCH")
.rows;
assert_eq!(rows.len(), 1, "exactly one User node must exist");
assert_eq!(
rows[0][0],
Value::String(evil.to_string()),
"name must equal the literal attacker string, unmodified"
);
let role_rows = db
.execute("MATCH (n:User) RETURN n.role")
.expect("MATCH")
.rows;
assert_eq!(role_rows.len(), 1);
assert_eq!(
role_rows[0][0],
Value::Null,
"no role property must have been created by the injection payload"
);
}
#[test]
fn injection_payload_with_closing_brace_and_paren() {
let (_dir, db) = make_db();
let evil = "x\"}) CREATE (:Pwned {y:\"1";
db.execute_with_params(
"CREATE (:User {name: $name})",
params(&[("name", Value::String(evil.to_string()))]),
)
.expect("parameterized CREATE must succeed");
let rows = db
.execute("MATCH (n:User) RETURN n.name")
.expect("MATCH")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::String(evil.to_string()));
let pwned = db.execute("MATCH (n:Pwned) RETURN n").expect("MATCH").rows;
assert_eq!(
pwned.len(),
0,
"the embedded second CREATE clause must never have executed"
);
}
#[test]
fn injection_payload_with_newline() {
let (_dir, db) = make_db();
let evil = "line one\nCREATE (:Pwned)\nline two";
db.execute_with_params(
"CREATE (:User {name: $name})",
params(&[("name", Value::String(evil.to_string()))]),
)
.expect("parameterized CREATE must succeed");
let rows = db
.execute("MATCH (n:User) RETURN n.name")
.expect("MATCH")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::String(evil.to_string()));
assert_eq!(
db.execute("MATCH (n:Pwned) RETURN n").unwrap().rows.len(),
0
);
}
#[test]
fn injection_payload_with_comment_sequence() {
let (_dir, db) = make_db();
let evil = "innocent // CREATE (:Pwned) RETURN n";
db.execute_with_params(
"CREATE (:User {name: $name})",
params(&[("name", Value::String(evil.to_string()))]),
)
.expect("parameterized CREATE must succeed");
let rows = db
.execute("MATCH (n:User) RETURN n.name")
.expect("MATCH")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::String(evil.to_string()));
assert_eq!(
db.execute("MATCH (n:Pwned) RETURN n").unwrap().rows.len(),
0
);
}
#[test]
fn injection_payload_with_double_quote() {
let (_dir, db) = make_db();
let evil = "a\"b\"c";
db.execute_with_params(
"CREATE (:User {name: $name})",
params(&[("name", Value::String(evil.to_string()))]),
)
.expect("parameterized CREATE must succeed");
let rows = db
.execute("MATCH (n:User) RETURN n.name")
.expect("MATCH")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::String(evil.to_string()));
}
#[test]
fn injection_payload_with_single_quote() {
let (_dir, db) = make_db();
let evil = "a'b'c";
db.execute_with_params(
"CREATE (:User {name: $name})",
params(&[("name", Value::String(evil.to_string()))]),
)
.expect("parameterized CREATE must succeed");
let rows = db
.execute("MATCH (n:User) RETURN n.name")
.expect("MATCH")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::String(evil.to_string()));
}
#[test]
fn injection_payload_full_second_statement_no_pwned_node() {
let (_dir, db) = make_db();
let evil = "x\"}) CREATE (:Pwned {y:\"1";
db.execute_with_params(
"CREATE (:User {name: $name})",
params(&[("name", Value::String(evil.to_string()))]),
)
.expect("parameterized CREATE must succeed");
assert_eq!(
db.execute("MATCH (n:Pwned) RETURN n").unwrap().rows.len(),
0,
"no :Pwned node must exist"
);
assert_eq!(
db.execute("MATCH (n:User) RETURN n").unwrap().rows.len(),
1,
"exactly one :User node must exist — the second CREATE never ran"
);
}
#[test]
fn interpolated_form_actually_injects_demonstrating_the_bug() {
let (_dir, db) = make_db();
let evil = "\", role: \"admin";
let cypher = format!("CREATE (:User {{name: \"{evil}\"}})");
db.execute(&cypher)
.expect("the interpolated (vulnerable) form parses and runs");
let rows = db
.execute("MATCH (n:User) RETURN n.name, n.role")
.expect("MATCH")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0][0],
Value::String(String::new()),
"interpolation truncated the name to an empty string, as the bug report describes"
);
assert_eq!(
rows[0][1],
Value::String("admin".to_string()),
"interpolation let the attacker inject a role:\"admin\" property — \
this is the vulnerability that parameterized CREATE exists to avoid"
);
}
#[test]
fn roundtrip_string_param() {
let (_dir, db) = make_db();
db.execute_with_params(
"CREATE (:Item {label: $v})",
params(&[("v", Value::String("hello world".to_string()))]),
)
.expect("CREATE with string param");
let rows = db.execute("MATCH (n:Item) RETURN n.label").unwrap().rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::String("hello world".to_string()));
}
#[test]
fn roundtrip_int_param() {
let (_dir, db) = make_db();
db.execute_with_params(
"CREATE (:Item {count: $v})",
params(&[("v", Value::Int64(-42))]),
)
.expect("CREATE with int param");
let rows = db.execute("MATCH (n:Item) RETURN n.count").unwrap().rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::Int64(-42));
}
#[test]
fn roundtrip_float_param() {
let (_dir, db) = make_db();
db.execute_with_params(
"CREATE (:Item {ratio: $v})",
params(&[("v", Value::Float64(12.375))]),
)
.expect("CREATE with float param");
let rows = db.execute("MATCH (n:Item) RETURN n.ratio").unwrap().rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::Float64(12.375));
}
#[test]
fn roundtrip_bool_param_matches_existing_int64_convention() {
let (_dir, db) = make_db();
db.execute_with_params(
"CREATE (:Item {active: $v})",
params(&[("v", Value::Bool(true))]),
)
.expect("CREATE with bool param");
let rows = db.execute("MATCH (n:Item) RETURN n.active").unwrap().rows;
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0][0],
Value::Int64(1),
"bool true round-trips as Int64(1), matching literal_to_value's convention"
);
}
#[test]
fn null_param_in_create_property_is_rejected_not_coerced_to_zero() {
let (_dir, db) = make_db();
let result = db.execute_with_params("CREATE (:Item {v: $v})", params(&[("v", Value::Null)]));
assert!(
result.is_err(),
"a null-valued $param in a CREATE property must error"
);
assert_eq!(
db.execute("MATCH (n:Item) RETURN n").unwrap().rows.len(),
0,
"the CREATE must not have run at all, not run with v silently set to 0"
);
}
#[test]
fn list_param_in_create_property_is_rejected_not_coerced_to_zero() {
let (_dir, db) = make_db();
let result = db.execute_with_params(
"CREATE (:Item {v: $v})",
params(&[("v", Value::List(vec![Value::Int64(1), Value::Int64(2)]))]),
);
assert!(
result.is_err(),
"a List-valued $param in a CREATE property must error"
);
assert_eq!(db.execute("MATCH (n:Item) RETURN n").unwrap().rows.len(), 0);
}
#[test]
fn map_param_in_create_property_is_rejected_not_coerced_to_zero() {
let (_dir, db) = make_db();
let result = db.execute_with_params(
"CREATE (:Item {v: $v})",
params(&[("v", Value::Map(vec![("k".to_string(), Value::Int64(1))]))]),
);
assert!(
result.is_err(),
"a Map-valued $param in a CREATE property must error"
);
assert_eq!(db.execute("MATCH (n:Item) RETURN n").unwrap().rows.len(), 0);
}
#[test]
fn unbound_param_in_create_property_errors() {
let (_dir, db) = make_db();
let result = db.execute_with_params("CREATE (:Item {v: $missing})", HashMap::new());
assert!(result.is_err(), "unbound $param must error");
}
#[test]
fn param_reference_via_plain_execute_without_params_errors() {
let (_dir, db) = make_db();
let result = db.execute("CREATE (:Item {v: $v})");
assert!(
result.is_err(),
"a $param reference with no params map supplied must error, not write 0"
);
assert_eq!(db.execute("MATCH (n:Item) RETURN n").unwrap().rows.len(), 0);
}
#[test]
fn standalone_create_edge_property_param_roundtrips() {
let (_dir, db) = make_db();
db.execute_with_params(
"CREATE (a:Node {name: 'A'})-[:LINK {weight: $w}]->(b:Node {name: 'B'})",
params(&[("w", Value::Int64(7))]),
)
.expect("standalone CREATE with edge $param");
let rows = db
.execute("MATCH (:Node {name: 'A'})-[r:LINK]->(:Node {name: 'B'}) RETURN r.weight")
.expect("MATCH edge")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::Int64(7));
}
#[test]
fn match_create_edge_property_param_roundtrips() {
let (_dir, db) = make_db();
db.execute("CREATE (:Node {name: 'A'})").unwrap();
db.execute("CREATE (:Node {name: 'B'})").unwrap();
db.execute_with_params(
"MATCH (a:Node {name: 'A'}), (b:Node {name: 'B'}) \
CREATE (a)-[:LINK {weight: $w}]->(b)",
params(&[("w", Value::Float64(2.5))]),
)
.expect("MATCH...CREATE with edge $param");
let rows = db
.execute("MATCH (:Node {name: 'A'})-[r:LINK]->(:Node {name: 'B'}) RETURN r.weight")
.expect("MATCH edge")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::Float64(2.5));
}
#[test]
fn match_create_edge_property_param_injection_payload_stored_literally() {
let (_dir, db) = make_db();
db.execute("CREATE (:Node {name: 'A'})").unwrap();
db.execute("CREATE (:Node {name: 'B'})").unwrap();
let evil = "x\"}]->(b) CREATE (:Pwned {y:\"1";
db.execute_with_params(
"MATCH (a:Node {name: 'A'}), (b:Node {name: 'B'}) \
CREATE (a)-[:LINK {label: $l}]->(b)",
params(&[("l", Value::String(evil.to_string()))]),
)
.expect("MATCH...CREATE with edge $param injection payload");
let rows = db
.execute("MATCH (:Node {name: 'A'})-[r:LINK]->(:Node {name: 'B'}) RETURN r.label")
.expect("MATCH edge")
.rows;
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], Value::String(evil.to_string()));
assert_eq!(
db.execute("MATCH (n:Pwned) RETURN n").unwrap().rows.len(),
0
);
}