forked from bitcoinafterlife/bal-server
- Add docs/INDEX.md with navigable index and quick reference guides - Add 9 knowledge base files covering project overview, Bitcoin domain, architecture, modules, API reference, database schema, deployment/security - Update AGENTS.md with knowledge base reference and update policy - Add tests/sql_injection_tests.rs with regression tests for SQL injection - Fix SQL injection vulnerabilities in bal-pusher.rs: * Replace string-formatted UPDATE IN with loop + parameterized queries * Replace string-formatted UPDATE push_err with parameterized query * Add chain name validation in calculate_stats to prevent env var tampering - Update .gitignore to exclude bal-pusher.env and bal-pusher.sh
113 lines
4.8 KiB
Rust
113 lines
4.8 KiB
Rust
use sqlite::{Connection, Value};
|
|
|
|
#[test]
|
|
fn test_sql_injection_via_push_err_update() {
|
|
// Create an in-memory database and the required table
|
|
let db = Connection::open(":memory:").unwrap();
|
|
let _ = db.execute(
|
|
"CREATE TABLE tbl_tx (txid TEXT PRIMARY KEY, status INTEGER, push_err TEXT);"
|
|
);
|
|
|
|
// Insert a dummy transaction
|
|
let mut stmt = db.prepare("INSERT INTO tbl_tx (txid, status, push_err) VALUES (?, ?, ?);").unwrap();
|
|
stmt.bind((1, Value::String("dummy_txid".to_string()))).unwrap();
|
|
stmt.bind((2, Value::Integer(0))).unwrap();
|
|
stmt.bind((3, Value::String("".to_string()))).unwrap();
|
|
let _ = stmt.next();
|
|
drop(stmt);
|
|
|
|
// Malicious error payload containing a single quote (SQL injection attempt)
|
|
let malicious_error = "'; DROP TABLE tbl_tx; --";
|
|
let txid = "dummy_txid";
|
|
|
|
// Execute the fixed query using parameter binding (safe)
|
|
let sql = "UPDATE tbl_tx SET status = 2, push_err = ? WHERE txid = ?";
|
|
let mut stmt = db.prepare(sql).unwrap();
|
|
stmt.bind((1, Value::String(malicious_error.to_string()))).unwrap();
|
|
stmt.bind((2, Value::String(txid.to_string()))).unwrap();
|
|
let _ = stmt.next();
|
|
|
|
// Verify the table still exists and the row was updated correctly
|
|
let mut check = db.prepare("SELECT status, push_err FROM tbl_tx WHERE txid = ?;").unwrap();
|
|
check.bind((1, Value::String("dummy_txid".to_string()))).unwrap();
|
|
assert!(check.next().unwrap() == sqlite::State::Row);
|
|
let status: i64 = check.read("status").unwrap();
|
|
let push_err: String = check.read("push_err").unwrap();
|
|
assert_eq!(status, 2);
|
|
assert_eq!(push_err, malicious_error);
|
|
|
|
// Ensure no second row was created (injection would have failed or produced extra rows)
|
|
let mut count_stmt = db.prepare("SELECT COUNT(*) FROM tbl_tx;").unwrap();
|
|
assert!(count_stmt.next().unwrap() == sqlite::State::Row);
|
|
let count: i64 = count_stmt.read(0).unwrap();
|
|
assert_eq!(count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sql_injection_via_txid_update() {
|
|
let db = Connection::open(":memory:").unwrap();
|
|
let _ = db.execute(
|
|
"CREATE TABLE tbl_tx (txid TEXT PRIMARY KEY, status INTEGER);"
|
|
);
|
|
|
|
// Insert multiple dummy transactions
|
|
for i in 0..3 {
|
|
let mut stmt = db.prepare("INSERT INTO tbl_tx (txid, status) VALUES (?, ?);").unwrap();
|
|
stmt.bind((1, Value::String(format!("txid_{}", i)))).unwrap();
|
|
stmt.bind((2, Value::Integer(0))).unwrap();
|
|
let _ = stmt.next();
|
|
}
|
|
|
|
// Malicious txid payload
|
|
let malicious_txid = "' OR '1'='1";
|
|
|
|
// The fixed query parameterizes the txid, so this should only update zero rows
|
|
let sql = "UPDATE tbl_tx SET status = 1 WHERE txid = ?";
|
|
let mut stmt = db.prepare(sql).unwrap();
|
|
stmt.bind((1, Value::String(malicious_txid.to_string()))).unwrap();
|
|
let _ = stmt.next();
|
|
|
|
// Verify no rows were updated (status should still be 0 for all)
|
|
for i in 0..3 {
|
|
let mut check = db.prepare("SELECT status FROM tbl_tx WHERE txid = ?;").unwrap();
|
|
check.bind((1, Value::String(format!("txid_{}", i)))).unwrap();
|
|
assert!(check.next().unwrap() == sqlite::State::Row);
|
|
let status: i64 = check.read("status").unwrap();
|
|
assert_eq!(status, 0, "Row txid_{} should not be updated by malicious txid", i);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_sql_injection_via_txid_with_comment() {
|
|
let db = Connection::open(":memory:").unwrap();
|
|
let _ = db.execute(
|
|
"CREATE TABLE tbl_tx (txid TEXT PRIMARY KEY, status INTEGER);"
|
|
);
|
|
|
|
let mut stmt = db.prepare("INSERT INTO tbl_tx (txid, status) VALUES (?, ?);").unwrap();
|
|
stmt.bind((1, Value::String("safe_txid".to_string()))).unwrap();
|
|
stmt.bind((2, Value::Integer(0))).unwrap();
|
|
let _ = stmt.next();
|
|
|
|
// Another common injection pattern
|
|
let malicious_txid = "safe_txid'; UPDATE tbl_tx SET status = 99; --";
|
|
|
|
let sql = "UPDATE tbl_tx SET status = 1 WHERE txid = ?";
|
|
let mut stmt = db.prepare(sql).unwrap();
|
|
stmt.bind((1, Value::String(malicious_txid.to_string()))).unwrap();
|
|
let _ = stmt.next();
|
|
|
|
// Verify the original row was NOT updated (because it was looking for the full malicious string)
|
|
// and no rows have status 99 (the injected update did not execute)
|
|
let mut check = db.prepare("SELECT status FROM tbl_tx WHERE txid = ?;").unwrap();
|
|
check.bind((1, Value::String("safe_txid".to_string()))).unwrap();
|
|
assert!(check.next().unwrap() == sqlite::State::Row);
|
|
let status: i64 = check.read("status").unwrap();
|
|
assert_eq!(status, 0, "Original row should not be updated");
|
|
|
|
let mut count_stmt = db.prepare("SELECT COUNT(*) FROM tbl_tx WHERE status = 99;").unwrap();
|
|
assert!(count_stmt.next().unwrap() == sqlite::State::Row);
|
|
let count: i64 = count_stmt.read(0).unwrap();
|
|
assert_eq!(count, 0, "No rows should have status 99");
|
|
}
|