Skip to content

Commit 229dd05

Browse files
committed
feat(backends): implement native prepared statements for PostgreSQL and SQLite
Override select_prepared() and execute_prepared() in both backends to use wire-level parameter binding instead of string interpolation: - PostgreSQL (pqxx): uses pqxx::params + exec_params() - PostgreSQL (libpq): uses PQexecParams() with paramValues array - SQLite: uses sqlite3_prepare_v2() + sqlite3_bind_*() + sqlite3_step() Add SQL injection tests verifying prepared statement path blocks injection attempts, handles typed parameters, and manages NULL values. Part of #557 (Phase 2 of 3)
1 parent 921b04a commit 229dd05

5 files changed

Lines changed: 643 additions & 0 deletions

File tree

database/backends/postgresql_backend.cpp

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,325 @@ kcenon::common::VoidResult postgresql_backend::execute_query(const std::string&
390390
#endif
391391
}
392392

393+
kcenon::common::Result<core::database_result> postgresql_backend::select_prepared(
394+
const std::string& query,
395+
const std::vector<core::database_value>& params)
396+
{
397+
if (!is_initialized()) {
398+
last_error_ = "Backend not initialized";
399+
return kcenon::common::error_info{
400+
static_cast<int>(database::error_code::invalid_state),
401+
last_error_,
402+
"postgresql_backend"
403+
};
404+
}
405+
406+
core::database_result result;
407+
408+
#ifdef USE_POSTGRESQL
409+
if (!connection_) {
410+
last_error_ = "No active connection";
411+
return kcenon::common::error_info{
412+
static_cast<int>(database::error_code::connection_failed),
413+
last_error_,
414+
"postgresql_backend"
415+
};
416+
}
417+
try {
418+
pqxx::connection* conn = static_cast<pqxx::connection*>(connection_);
419+
pqxx::work txn(*conn);
420+
421+
// Build pqxx::params from database_value vector
422+
pqxx::params pq_params;
423+
for (const auto& val : params) {
424+
std::visit([&pq_params](const auto& v) {
425+
using T = std::decay_t<decltype(v)>;
426+
if constexpr (std::is_same_v<T, std::nullptr_t>) {
427+
pq_params.append();
428+
} else if constexpr (std::is_same_v<T, bool>) {
429+
pq_params.append(v);
430+
} else if constexpr (std::is_same_v<T, int64_t>) {
431+
pq_params.append(v);
432+
} else if constexpr (std::is_same_v<T, double>) {
433+
pq_params.append(v);
434+
} else if constexpr (std::is_same_v<T, std::string>) {
435+
pq_params.append(v);
436+
}
437+
}, val);
438+
}
439+
440+
pqxx::result pqxx_result = txn.exec_params(query, pq_params);
441+
txn.commit();
442+
443+
for (const auto& row : pqxx_result) {
444+
core::database_row db_row;
445+
for (size_t i = 0; i < row.size(); ++i) {
446+
std::string column_name = pqxx_result.column_name(i);
447+
if (row[i].is_null()) {
448+
db_row[column_name] = nullptr;
449+
} else {
450+
if (row[i].type() == PG_INT8OID ||
451+
row[i].type() == PG_INT4OID) {
452+
db_row[column_name] = row[i].as<int64_t>();
453+
} else if (row[i].type() == PG_FLOAT8OID ||
454+
row[i].type() == PG_FLOAT4OID) {
455+
db_row[column_name] = row[i].as<double>();
456+
} else if (row[i].type() == PG_BOOLOID) {
457+
db_row[column_name] = row[i].as<bool>();
458+
} else {
459+
db_row[column_name] = row[i].as<std::string>();
460+
}
461+
}
462+
}
463+
result.push_back(std::move(db_row));
464+
}
465+
} catch (const std::exception& e) {
466+
last_error_ = std::string("Select prepared error: ") + e.what();
467+
logger_.error("select_prepared", last_error_);
468+
return kcenon::common::error_info{
469+
static_cast<int>(database::error_code::query_failed),
470+
last_error_,
471+
"postgresql_backend"
472+
};
473+
}
474+
#elif defined(HAVE_LIBPQ)
475+
if (!connection_) {
476+
last_error_ = "No active connection";
477+
return kcenon::common::error_info{
478+
static_cast<int>(database::error_code::connection_failed),
479+
last_error_,
480+
"postgresql_backend"
481+
};
482+
}
483+
try {
484+
// Convert params to C string array for PQexecParams
485+
std::vector<std::string> param_strings;
486+
std::vector<const char*> param_values;
487+
param_strings.reserve(params.size());
488+
param_values.reserve(params.size());
489+
490+
for (const auto& val : params) {
491+
std::visit([&param_strings, &param_values](const auto& v) {
492+
using T = std::decay_t<decltype(v)>;
493+
if constexpr (std::is_same_v<T, std::nullptr_t>) {
494+
param_strings.emplace_back();
495+
param_values.push_back(nullptr);
496+
} else if constexpr (std::is_same_v<T, bool>) {
497+
param_strings.push_back(v ? "t" : "f");
498+
param_values.push_back(param_strings.back().c_str());
499+
} else if constexpr (std::is_same_v<T, std::string>) {
500+
param_strings.push_back(v);
501+
param_values.push_back(param_strings.back().c_str());
502+
} else {
503+
param_strings.push_back(std::to_string(v));
504+
param_values.push_back(param_strings.back().c_str());
505+
}
506+
}, val);
507+
}
508+
509+
PGresult* pg_result = PQexecParams(
510+
static_cast<PGconn*>(connection_),
511+
query.c_str(),
512+
static_cast<int>(params.size()),
513+
nullptr, // let server infer types
514+
param_values.data(),
515+
nullptr, // text format lengths
516+
nullptr, // text format
517+
0 // text result format
518+
);
519+
520+
if (PQresultStatus(pg_result) != PGRES_TUPLES_OK) {
521+
last_error_ = PQerrorMessage(static_cast<PGconn*>(connection_));
522+
PQclear(pg_result);
523+
return kcenon::common::error_info{
524+
static_cast<int>(database::error_code::query_failed),
525+
last_error_,
526+
"postgresql_backend"
527+
};
528+
}
529+
530+
int rows = PQntuples(pg_result);
531+
int cols = PQnfields(pg_result);
532+
533+
for (int row = 0; row < rows; ++row) {
534+
core::database_row db_row;
535+
for (int col = 0; col < cols; ++col) {
536+
std::string column_name = PQfname(pg_result, col);
537+
if (PQgetisnull(pg_result, row, col)) {
538+
db_row[column_name] = nullptr;
539+
} else {
540+
const char* value = PQgetvalue(pg_result, row, col);
541+
Oid type = PQftype(pg_result, col);
542+
543+
if (type == 20 || type == 21 || type == 23) {
544+
db_row[column_name] = static_cast<int64_t>(std::stoll(value));
545+
} else if (type == 700 || type == 701) {
546+
db_row[column_name] = std::stod(value);
547+
} else if (type == 16) {
548+
db_row[column_name] = (*value == 't' || *value == '1');
549+
} else {
550+
db_row[column_name] = std::string(value);
551+
}
552+
}
553+
}
554+
result.push_back(std::move(db_row));
555+
}
556+
PQclear(pg_result);
557+
} catch (const std::exception& e) {
558+
last_error_ = std::string("Select prepared error: ") + e.what();
559+
logger_.error("select_prepared", last_error_);
560+
return kcenon::common::error_info{
561+
static_cast<int>(database::error_code::query_failed),
562+
last_error_,
563+
"postgresql_backend"
564+
};
565+
}
566+
#else
567+
// Fallback to string interpolation for mock mode
568+
return database_backend::select_prepared(query, params);
569+
#endif
570+
571+
last_error_.clear();
572+
return result;
573+
}
574+
575+
kcenon::common::VoidResult postgresql_backend::execute_prepared(
576+
const std::string& query,
577+
const std::vector<core::database_value>& params)
578+
{
579+
if (!is_initialized()) {
580+
last_error_ = "Backend not initialized";
581+
return kcenon::common::error_info{
582+
static_cast<int>(database::error_code::invalid_state),
583+
last_error_,
584+
"postgresql_backend"
585+
};
586+
}
587+
588+
#ifdef USE_POSTGRESQL
589+
try {
590+
if (!connection_) {
591+
last_error_ = "No active PostgreSQL connection";
592+
logger_.error("execute_prepared", last_error_);
593+
return kcenon::common::error_info{
594+
static_cast<int>(database::error_code::connection_failed),
595+
last_error_,
596+
"postgresql_backend"
597+
};
598+
}
599+
600+
pqxx::connection* conn = static_cast<pqxx::connection*>(connection_);
601+
pqxx::work txn(*conn);
602+
603+
pqxx::params pq_params;
604+
for (const auto& val : params) {
605+
std::visit([&pq_params](const auto& v) {
606+
using T = std::decay_t<decltype(v)>;
607+
if constexpr (std::is_same_v<T, std::nullptr_t>) {
608+
pq_params.append();
609+
} else if constexpr (std::is_same_v<T, bool>) {
610+
pq_params.append(v);
611+
} else if constexpr (std::is_same_v<T, int64_t>) {
612+
pq_params.append(v);
613+
} else if constexpr (std::is_same_v<T, double>) {
614+
pq_params.append(v);
615+
} else if constexpr (std::is_same_v<T, std::string>) {
616+
pq_params.append(v);
617+
}
618+
}, val);
619+
}
620+
621+
txn.exec_params(query, pq_params);
622+
txn.commit();
623+
last_error_.clear();
624+
return kcenon::common::ok();
625+
} catch (const std::exception& e) {
626+
last_error_ = std::string("Execute prepared error: ") + e.what();
627+
logger_.error("execute_prepared", last_error_);
628+
return kcenon::common::error_info{
629+
static_cast<int>(database::error_code::query_failed),
630+
last_error_,
631+
"postgresql_backend"
632+
};
633+
}
634+
#elif defined(HAVE_LIBPQ)
635+
if (!connection_) {
636+
last_error_ = "No active PostgreSQL connection";
637+
logger_.error("execute_prepared", last_error_);
638+
return kcenon::common::error_info{
639+
static_cast<int>(database::error_code::connection_failed),
640+
last_error_,
641+
"postgresql_backend"
642+
};
643+
}
644+
645+
std::vector<std::string> param_strings;
646+
std::vector<const char*> param_values;
647+
param_strings.reserve(params.size());
648+
param_values.reserve(params.size());
649+
650+
for (const auto& val : params) {
651+
std::visit([&param_strings, &param_values](const auto& v) {
652+
using T = std::decay_t<decltype(v)>;
653+
if constexpr (std::is_same_v<T, std::nullptr_t>) {
654+
param_strings.emplace_back();
655+
param_values.push_back(nullptr);
656+
} else if constexpr (std::is_same_v<T, bool>) {
657+
param_strings.push_back(v ? "t" : "f");
658+
param_values.push_back(param_strings.back().c_str());
659+
} else if constexpr (std::is_same_v<T, std::string>) {
660+
param_strings.push_back(v);
661+
param_values.push_back(param_strings.back().c_str());
662+
} else {
663+
param_strings.push_back(std::to_string(v));
664+
param_values.push_back(param_strings.back().c_str());
665+
}
666+
}, val);
667+
}
668+
669+
PGresult* pg_result = PQexecParams(
670+
static_cast<PGconn*>(connection_),
671+
query.c_str(),
672+
static_cast<int>(params.size()),
673+
nullptr,
674+
param_values.data(),
675+
nullptr,
676+
nullptr,
677+
0
678+
);
679+
680+
if (pg_result == nullptr) {
681+
last_error_ = "PostgreSQL execute prepared failed";
682+
logger_.error("execute_prepared", last_error_);
683+
return kcenon::common::error_info{
684+
static_cast<int>(database::error_code::query_failed),
685+
last_error_,
686+
"postgresql_backend"
687+
};
688+
}
689+
690+
ExecStatusType status = PQresultStatus(pg_result);
691+
bool success = (status == PGRES_COMMAND_OK) || (status == PGRES_TUPLES_OK);
692+
693+
if (!success) {
694+
last_error_ = PQerrorMessage(static_cast<PGconn*>(connection_));
695+
logger_.error("execute_prepared", last_error_);
696+
PQclear(pg_result);
697+
return kcenon::common::error_info{
698+
static_cast<int>(database::error_code::query_failed),
699+
last_error_,
700+
"postgresql_backend"
701+
};
702+
}
703+
704+
PQclear(pg_result);
705+
last_error_.clear();
706+
return kcenon::common::ok();
707+
#else
708+
return database_backend::execute_prepared(query, params);
709+
#endif
710+
}
711+
393712
kcenon::common::VoidResult postgresql_backend::begin_transaction()
394713
{
395714
if (!is_initialized()) {

database/backends/postgresql_backend.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ class postgresql_backend
9393

9494
kcenon::common::VoidResult execute_query(const std::string& query_string) override;
9595

96+
[[nodiscard]] kcenon::common::Result<core::database_result> select_prepared(
97+
const std::string& query,
98+
const std::vector<core::database_value>& params) override;
99+
100+
[[nodiscard]] kcenon::common::VoidResult execute_prepared(
101+
const std::string& query,
102+
const std::vector<core::database_value>& params) override;
103+
96104
kcenon::common::VoidResult begin_transaction() override;
97105

98106
kcenon::common::VoidResult commit_transaction() override;

0 commit comments

Comments
 (0)