Database System 0.1.0
Advanced C++20 Database System with Multi-Backend Support
Loading...
Searching...
No Matches
sql_injection_test.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
14#include <gtest/gtest.h>
15#include <memory>
16#include <string>
17#include <vector>
18
22
23using namespace database;
24using namespace database::backends;
25using namespace database::core;
26
31class SQLInjectionTest : public ::testing::Test {
32protected:
33 std::unique_ptr<sqlite_backend> db_;
34
35 void SetUp() override {
36 db_ = std::make_unique<sqlite_backend>();
37#ifdef USE_SQLITE
38 connection_config config;
39 config.database = ":memory:";
40 ASSERT_TRUE(db_->initialize(config).is_ok());
41 ASSERT_TRUE(db_->execute_query("CREATE TABLE users ("
42 " id INTEGER PRIMARY KEY,"
43 " name TEXT,"
44 " email TEXT,"
45 " password_hash TEXT"
46 ")").is_ok());
47 // Insert test data
48 auto r1 = db_->execute_query(
49 "INSERT INTO users (id, name, email, password_hash) "
50 "VALUES (1, 'Alice', 'alice@test.com', 'hash123')"
51 );
52 ASSERT_TRUE(r1.is_ok());
53 auto r2 = db_->execute_query(
54 "INSERT INTO users (id, name, email, password_hash) "
55 "VALUES (2, 'Bob', 'bob@test.com', 'hash456')"
56 );
57 ASSERT_TRUE(r2.is_ok());
58#else
59 GTEST_SKIP() << "SQLite not available";
60#endif
61 }
62
63 void TearDown() override {
64 if (db_ && db_->is_initialized()) {
65 db_->shutdown();
66 }
67 }
68};
69
70//=============================================================================
71// Classic SQL Injection Attempts
72//=============================================================================
73
81TEST_F(SQLInjectionTest, BasicInjectionAttempt) {
82#ifdef USE_SQLITE
83 std::string malicious_input = "' OR '1'='1";
84
85 query_builder builder(database_types::sqlite);
86 auto query = builder
87 .select({"*"})
88 .from("users")
89 .where("name", "=", malicious_input)
90 .build();
91
92 auto query_result = db_->select_query(query);
93
94 // If properly escaped, should return 0 rows (no user named "' OR '1'='1")
95 // If vulnerable, would return all rows
96 if (query_result.is_ok()) {
97 EXPECT_LE(query_result.value().size(), 1u)
98 << "Possible SQL injection vulnerability: query returned multiple rows";
99 }
100#else
101 GTEST_SKIP() << "SQLite not available";
102#endif
103}
104
112TEST_F(SQLInjectionTest, CommentInjectionAttempt) {
113#ifdef USE_SQLITE
114 std::string malicious_input = "admin'--";
115
116 query_builder builder(database_types::sqlite);
117 auto query = builder
118 .select({"*"})
119 .from("users")
120 .where("name", "=", malicious_input)
121 .build();
122
123 auto query_result = db_->select_query(query);
124
125 // Should return 0 rows - no user named "admin'--"
126 if (query_result.is_ok()) {
127 EXPECT_EQ(query_result.value().size(), 0u);
128 }
129#else
130 GTEST_SKIP() << "SQLite not available";
131#endif
132}
133
141TEST_F(SQLInjectionTest, BatchStatementInjectionAttempt) {
142#ifdef USE_SQLITE
143 std::string malicious_input = "'; DROP TABLE users; --";
144
145 query_builder builder(database_types::sqlite);
146 auto query = builder
147 .select({"*"})
148 .from("users")
149 .where("name", "=", malicious_input)
150 .build();
151
152 // Execute the query (may fail, but shouldn't drop table)
153 db_->select_query(query);
154
155 // Verify table still exists by querying it
156 auto check = db_->select_query("SELECT COUNT(*) as cnt FROM users");
157 EXPECT_TRUE(check.is_ok() && !check.value().empty())
158 << "CRITICAL: Table appears to have been dropped!";
159#else
160 GTEST_SKIP() << "SQLite not available";
161#endif
162}
163
164//=============================================================================
165// Union-Based Injection Attempts
166//=============================================================================
167
175TEST_F(SQLInjectionTest, UnionInjectionAttempt) {
176#ifdef USE_SQLITE
177 // Create a sensitive table for the test
178 db_->execute_query(
179 "CREATE TABLE sensitive_data (secret TEXT)"
180 );
181 db_->execute_query(
182 "INSERT INTO sensitive_data VALUES ('top_secret_value')"
183 );
184
185 std::string malicious_input = "' UNION SELECT secret, secret, secret, secret FROM sensitive_data --";
186
187 query_builder builder(database_types::sqlite);
188 auto query = builder
189 .select({"*"})
190 .from("users")
191 .where("name", "=", malicious_input)
192 .build();
193
194 auto query_result = db_->select_query(query);
195
196 // Check that sensitive data was not leaked
197 bool found_secret = false;
198 if (query_result.is_ok()) {
199 for (const auto& row : query_result.value()) {
200 for (const auto& [key, value] : row) {
201 if (std::holds_alternative<std::string>(value)) {
202 if (std::get<std::string>(value).find("top_secret") != std::string::npos) {
203 found_secret = true;
204 }
205 }
206 }
207 }
208 }
209 EXPECT_FALSE(found_secret)
210 << "CRITICAL: Sensitive data leaked through UNION injection!";
211#else
212 GTEST_SKIP() << "SQLite not available";
213#endif
214}
215
216//=============================================================================
217// Parameterized Value Safety Tests
218//=============================================================================
219
227TEST_F(SQLInjectionTest, ApostropheInValueSafe) {
228#ifdef USE_SQLITE
229 // Insert a user with apostrophe in name
230 auto insert_result = db_->execute_query(
231 "INSERT INTO users (id, name, email, password_hash) "
232 "VALUES (3, 'O''Brien', 'obrien@test.com', 'hash789')"
233 );
234 (void)insert_result;
235
236 core::database_value safe_value = std::string("O'Brien");
237
238 query_builder builder(database_types::sqlite);
239 auto query = builder
240 .select({"*"})
241 .from("users")
242 .where("name", "=", safe_value)
243 .build();
244
245 // Check that apostrophe is escaped in built query
246 // Should contain either O''Brien (SQL standard) or O\'Brien (backslash-escaped)
247 bool properly_escaped =
248 (query.find("O''Brien") != std::string::npos) ||
249 (query.find("O\\'Brien") != std::string::npos) ||
250 (query.find("O'Brien") != std::string::npos); // May work if using prepared stmt
251
252 EXPECT_TRUE(properly_escaped)
253 << "Query should handle apostrophe safely. Built query: " << query;
254#else
255 GTEST_SKIP() << "SQLite not available";
256#endif
257}
258
263TEST_F(SQLInjectionTest, SpecialCharactersInValue) {
264#ifdef USE_SQLITE
265 std::vector<std::string> special_inputs = {
266 "test\\value", // Backslash
267 "test\"value", // Double quote
268 "test\nvalue", // Newline
269 "test\rvalue", // Carriage return
270 "test\tvalue", // Tab
271 "test%value", // Percent (LIKE wildcard)
272 "test_value", // Underscore (LIKE wildcard)
273 };
274
275 for (const auto& input : special_inputs) {
276 query_builder builder(database_types::sqlite);
277 auto query = builder
278 .select({"*"})
279 .from("users")
280 .where("name", "=", input)
281 .build();
282
283 // Query should build without throwing
284 EXPECT_FALSE(query.empty())
285 << "Failed to build query with special input: " << input;
286
287 // Query execution should not throw
288 EXPECT_NO_THROW({
289 db_->select_query(query);
290 }) << "Query execution failed with special input: " << input;
291 }
292#else
293 GTEST_SKIP() << "SQLite not available";
294#endif
295}
296
297//=============================================================================
298// Numeric Value Tests
299//=============================================================================
300
305TEST_F(SQLInjectionTest, NumericValueInjection) {
306#ifdef USE_SQLITE
307 // Attempting to inject via what should be a numeric field
308 core::database_value numeric_value = static_cast<int64_t>(1);
309
310 query_builder builder(database_types::sqlite);
311 auto query = builder
312 .select({"*"})
313 .from("users")
314 .where("id", "=", numeric_value)
315 .build();
316
317 // Numeric values should be rendered without quotes
318 EXPECT_TRUE(query.find("id = 1") != std::string::npos ||
319 query.find("[id] = 1") != std::string::npos)
320 << "Numeric value should not be quoted. Query: " << query;
321#else
322 GTEST_SKIP() << "SQLite not available";
323#endif
324}
325
330TEST_F(SQLInjectionTest, BooleanValueHandling) {
331#ifdef USE_SQLITE
332 core::database_value bool_value = true;
333
334 query_builder builder(database_types::sqlite);
335 auto query = builder
336 .select({"*"})
337 .from("users")
338 .where("id", ">", bool_value)
339 .build();
340
341 // Should render as TRUE or 1, not as quoted string
342 EXPECT_TRUE(query.find("TRUE") != std::string::npos ||
343 query.find("1") != std::string::npos)
344 << "Boolean value handling issue. Query: " << query;
345#else
346 GTEST_SKIP() << "SQLite not available";
347#endif
348}
349
350//=============================================================================
351// Prepared Statement Tests (Wire-Level Parameterization)
352//=============================================================================
353
358TEST_F(SQLInjectionTest, PreparedSelectReturnsCorrectResults) {
359#ifdef USE_SQLITE
360 std::vector<core::database_value> params = {std::string("Alice")};
361 auto result = db_->select_prepared(
362 "SELECT * FROM users WHERE name = ?", params);
363
364 ASSERT_TRUE(result.is_ok());
365 ASSERT_EQ(result.value().size(), 1u);
366
367 const auto& row = result.value()[0];
368 auto it = row.find("email");
369 ASSERT_NE(it, row.end());
370 EXPECT_EQ(std::get<std::string>(it->second), "alice@test.com");
371#else
372 GTEST_SKIP() << "SQLite not available";
373#endif
374}
375
380TEST_F(SQLInjectionTest, PreparedSelectBlocksInjection) {
381#ifdef USE_SQLITE
382 std::string malicious = "' OR '1'='1";
383 std::vector<core::database_value> params = {malicious};
384 auto result = db_->select_prepared(
385 "SELECT * FROM users WHERE name = ?", params);
386
387 ASSERT_TRUE(result.is_ok());
388 EXPECT_EQ(result.value().size(), 0u)
389 << "Prepared statement should treat malicious input as literal string";
390#else
391 GTEST_SKIP() << "SQLite not available";
392#endif
393}
394
399TEST_F(SQLInjectionTest, PreparedExecuteWithTypedParams) {
400#ifdef USE_SQLITE
401 std::vector<core::database_value> params = {
402 int64_t(10),
403 std::string("Charlie"),
404 std::string("charlie@test.com"),
405 std::string("hash_prep")
406 };
407 auto exec_result = db_->execute_prepared(
408 "INSERT INTO users (id, name, email, password_hash) VALUES (?, ?, ?, ?)",
409 params);
410 ASSERT_TRUE(exec_result.is_ok());
411
412 // Verify the row was inserted
413 std::vector<core::database_value> select_params = {int64_t(10)};
414 auto select_result = db_->select_prepared(
415 "SELECT name FROM users WHERE id = ?", select_params);
416 ASSERT_TRUE(select_result.is_ok());
417 ASSERT_EQ(select_result.value().size(), 1u);
418 EXPECT_EQ(std::get<std::string>(select_result.value()[0].at("name")), "Charlie");
419#else
420 GTEST_SKIP() << "SQLite not available";
421#endif
422}
423
428TEST_F(SQLInjectionTest, PreparedBatchStatementInjectionBlocked) {
429#ifdef USE_SQLITE
430 std::string malicious = "'; DROP TABLE users; --";
431 std::vector<core::database_value> params = {malicious};
432 db_->select_prepared("SELECT * FROM users WHERE name = ?", params);
433
434 // Verify the table still exists
435 auto check = db_->select_query("SELECT COUNT(*) as cnt FROM users");
436 ASSERT_TRUE(check.is_ok());
437 EXPECT_FALSE(check.value().empty())
438 << "CRITICAL: Table dropped via prepared statement injection!";
439#else
440 GTEST_SKIP() << "SQLite not available";
441#endif
442}
443
448TEST_F(SQLInjectionTest, PreparedNullParameterHandling) {
449#ifdef USE_SQLITE
450 std::vector<core::database_value> params = {nullptr};
451 auto result = db_->select_prepared(
452 "SELECT * FROM users WHERE name = ?", params);
453
454 ASSERT_TRUE(result.is_ok());
455 EXPECT_EQ(result.value().size(), 0u)
456 << "NULL comparison should match no rows (NULL != NULL in SQL)";
457#else
458 GTEST_SKIP() << "SQLite not available";
459#endif
460}
461
462//=============================================================================
463// Query Builder State Tests
464//=============================================================================
465
470TEST_F(SQLInjectionTest, ResetPreventsDataLeakage) {
471 query_builder builder(database_types::sqlite);
472
473 // Build first query with sensitive filter
474 builder
475 .select({"*"})
476 .from("users")
477 .where("email", "=", std::string("admin@secret.com"));
478
479 // Reset and build new query
480 builder.reset();
481 auto query = builder
482 .select({"id"})
483 .from("public_data")
484 .build();
485
486 // Previous filter should not leak into new query
487 EXPECT_TRUE(query.find("admin@secret") == std::string::npos)
488 << "Previous query data leaked after reset! Query: " << query;
489 EXPECT_TRUE(query.find("users") == std::string::npos)
490 << "Previous table leaked after reset! Query: " << query;
491}
492
493//=============================================================================
494// Encoding Attack Tests
495//=============================================================================
496
501TEST_F(SQLInjectionTest, UnicodeBypassAttempt) {
502#ifdef USE_SQLITE
503 // Various Unicode representations that might bypass naive filters
504 std::vector<std::string> unicode_attacks = {
505 "\xc0\x27", // Overlong encoding of '
506 "\xe0\x80\xa7", // Another overlong encoding
507 "admin\xef\xbb\xbf", // With BOM
508 };
509
510 for (const auto& attack : unicode_attacks) {
511 query_builder builder(database_types::sqlite);
512 auto query = builder
513 .select({"*"})
514 .from("users")
515 .where("name", "=", attack)
516 .build();
517
518 // Should handle without crashing
519 EXPECT_NO_THROW({
520 db_->select_query(query);
521 }) << "Query failed with unicode attack: [binary data]";
522 }
523#else
524 GTEST_SKIP() << "SQLite not available";
525#endif
526}
Test fixture for SQL injection prevention tests.
void SetUp() override
std::unique_ptr< sqlite_backend > db_
void TearDown() override
Universal query builder that adapts to different database types.
query_builder & select(const std::vector< std::string > &columns)
std::string build() const
query_builder & where(const std::string &field, const std::string &op, const core::database_value &value)
Abstract interface for database backends.
std::variant< std::string, int64_t, double, bool, std::nullptr_t > database_value
TEST_F(SQLInjectionTest, BasicInjectionAttempt)
Tests that basic OR injection is properly escaped.
SQLite database backend plugin implementation.
Configuration for database connection.
#define ASSERT_EQ(expected, actual, message)
#define ASSERT_TRUE(condition, message)