diff --git a/docs/README.md b/docs/README.md index a719b86..f714591 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,6 +20,8 @@ [sqlgen::Literal](literal.md) - How to define fields that only allow for a fixed set of values. +[sqlgen::Pattern](pattern.md) - How to add regex pattern validation to avoid SQL injection. + ## Supported databases [postgres](postgres.md) - How to interact with PostgreSQL or a related database (Redshift, Aurora, Greenplum...). diff --git a/docs/pattern.md b/docs/pattern.md new file mode 100644 index 0000000..2c14115 --- /dev/null +++ b/docs/pattern.md @@ -0,0 +1,110 @@ +# `sqlgen::Pattern` + +The `sqlgen::Pattern` class provides a type-safe way to validate string fields against regular expressions in C++. It's particularly useful for preventing SQL injection by ensuring data conforms to expected patterns before it reaches the database. + +## Usage + +### Basic Definition + +Define a pattern using a regular expression and a descriptive name: + +```cpp +// Define a pattern for usernames (alphanumeric with underscores) +using Username = sqlgen::Pattern<"[a-zA-Z0-9_]+", "username">; + +struct User { + sqlgen::PrimaryKey id; + Username username; + sqlgen::Email email; +}; +``` + +### Built-in Patterns + +SQLGen provides several commonly used patterns out of the box: + +```cpp +// Alphanumeric strings +sqlgen::AlphaNumeric name; + +// Base64 encoded strings +sqlgen::Base64Encoded encoded_data; + +// Email addresses +sqlgen::Email email; + +// UUID formats +sqlgen::UUIDv1 uuid1; +sqlgen::UUIDv2 uuid2; +sqlgen::UUIDv3 uuid3; +sqlgen::UUIDv4 uuid4; +``` + +### Assignment and Validation + +Assign values to pattern-validated fields: + +```cpp +struct Person { + sqlgen::PrimaryKey id; + sqlgen::AlphaNumeric first_name; + sqlgen::AlphaNumeric last_name; + int age; +}; + +const auto person = Person{ + .id = 1, + .first_name = "Homer", // Valid: contains only alphanumeric characters + .last_name = "Simpson", // Valid: contains only alphanumeric characters + .age = 45 +}; +``` + +### Accessing Values + +Access the underlying string value: + +```cpp +const auto person = Person{ + .first_name = "Homer", + .last_name = "Simpson" +}; + +// Get the value +const std::string& value1 = person.first_name(); +const std::string& value2 = person.first_name.get(); +const std::string& value3 = person.first_name.value(); +``` + +## SQL Injection Prevention + +Pattern validation is a crucial security feature that helps prevent SQL injection attacks by: + +1. Enforcing strict input validation before data reaches the database +2. Ensuring data conforms to expected formats +3. Rejecting potentially malicious input that doesn't match the pattern + +For example, using `AlphaNumeric` for usernames prevents injection of SQL commands: + +```cpp +struct User { + sqlgen::PrimaryKey id; + sqlgen::AlphaNumeric username; // Rejects: "admin'; DROP TABLE users;--" + sqlgen::Email email; // Rejects: "invalid@email'; DELETE FROM users;--" +}; +``` + +## Notes + +- Pattern validation occurs at runtime when values are assigned +- The template parameters are: + - `_regex`: The regular expression pattern to match against + - `_name`: A descriptive name for the pattern (used in error messages) +- The class supports: + - Direct string assignment with validation + - Multiple access methods for the underlying value + - Reflection for SQL operations + - Move and copy semantics +- Built-in patterns provide common validation rules +- Custom patterns can be defined for specific use cases +- Pattern validation is an important part of a defense-in-depth security strategy diff --git a/include/sqlgen.hpp b/include/sqlgen.hpp index aadab46..2921460 100644 --- a/include/sqlgen.hpp +++ b/include/sqlgen.hpp @@ -6,6 +6,7 @@ #include "sqlgen/Iterator.hpp" #include "sqlgen/IteratorBase.hpp" #include "sqlgen/Literal.hpp" +#include "sqlgen/Pattern.hpp" #include "sqlgen/PrimaryKey.hpp" #include "sqlgen/Range.hpp" #include "sqlgen/Ref.hpp" @@ -14,6 +15,7 @@ #include "sqlgen/col.hpp" #include "sqlgen/limit.hpp" #include "sqlgen/order_by.hpp" +#include "sqlgen/patterns.hpp" #include "sqlgen/read.hpp" #include "sqlgen/where.hpp" #include "sqlgen/write.hpp" diff --git a/include/sqlgen/Pattern.hpp b/include/sqlgen/Pattern.hpp new file mode 100644 index 0000000..b3e3ed7 --- /dev/null +++ b/include/sqlgen/Pattern.hpp @@ -0,0 +1,15 @@ +#ifndef SQLGEN_PATTERN_HPP_ +#define SQLGEN_PATTERN_HPP_ + +#include + +namespace sqlgen { + +template +using Pattern = rfl::Pattern<_regex, _name>; + +}; // namespace sqlgen + +#endif + diff --git a/include/sqlgen/patterns.hpp b/include/sqlgen/patterns.hpp new file mode 100644 index 0000000..f0a3e22 --- /dev/null +++ b/include/sqlgen/patterns.hpp @@ -0,0 +1,25 @@ +#ifndef SQLGEN_PATTERNS_HPP_ +#define SQLGEN_PATTERNS_HPP_ + +#include + +namespace sqlgen { + +using AlphaNumeric = rfl::AlphaNumeric; + +using Base64Encoded = rfl::Base64Encoded; + +using Email = rfl::Email; + +using UUIDv1 = rfl::UUIDv1; + +using UUIDv2 = rfl::UUIDv2; + +using UUIDv3 = rfl::UUIDv3; + +using UUIDv4 = rfl::UUIDv4; + +} // namespace sqlgen + +#endif + diff --git a/tests/sqlite/test_alpha_numeric.cpp b/tests/sqlite/test_alpha_numeric.cpp new file mode 100644 index 0000000..086105a --- /dev/null +++ b/tests/sqlite/test_alpha_numeric.cpp @@ -0,0 +1,39 @@ +#include + +#include +#include +#include +#include +#include + +namespace test_alpha_numeric { + +struct Person { + sqlgen::PrimaryKey id; + sqlgen::AlphaNumeric first_name; + sqlgen::AlphaNumeric last_name; + int age; +}; + +TEST(sqlite, test_alpha_numeric) { + const auto people1 = std::vector( + {Person{ + .id = 0, .first_name = "Homer", .last_name = "Simpson", .age = 45}, + Person{.id = 1, .first_name = "Bart", .last_name = "Simpson", .age = 10}, + Person{.id = 2, .first_name = "Lisa", .last_name = "Simpson", .age = 8}, + Person{ + .id = 3, .first_name = "Maggie", .last_name = "Simpson", .age = 0}}); + + const auto conn = sqlgen::sqlite::connect(); + + sqlgen::write(conn, people1); + + const auto people2 = sqlgen::read>(conn).value(); + + const auto json1 = rfl::json::write(people1); + const auto json2 = rfl::json::write(people2); + + EXPECT_EQ(json1, json2); +} + +} // namespace test_alpha_numeric