code: hpp,dbal,cpp (16 files)

This commit is contained in:
2025-12-26 01:26:19 +00:00
parent f12ff05af3
commit bbd8acb296
16 changed files with 521 additions and 4 deletions

View File

@@ -14,21 +14,21 @@ namespace entities {
namespace session {
/**
* Delete a session by ID (logout)
* Delete a session by ID
*/
inline Result<bool> remove(InMemoryStore& store, const std::string& id) {
if (id.empty()) {
return Error::validationError("Session ID cannot be empty");
}
auto it = store.sessions.find(id);
if (it == store.sessions.end()) {
return Error::notFound("Session not found: " + id);
}
store.session_tokens.erase(it->second.token);
store.sessions.erase(it);
return Result<bool>(true);
}

View File

@@ -0,0 +1,39 @@
#pragma once
/**
* @file contains_sql_keyword.hpp
* @brief SQL keyword detection
*/
#include <string>
#include <algorithm>
#include <cctype>
namespace dbal::security {
/**
* Check if string matches a SQL keyword (case-insensitive)
* @param value String to check
* @return true if matches SQL keyword
*/
inline bool contains_sql_keyword(const std::string& value) {
static const char* keywords[] = {
"SELECT", "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER",
"TRUNCATE", "GRANT", "REVOKE", "UNION", "JOIN", "WHERE", "FROM",
"TABLE", "DATABASE", "INDEX", "VIEW", "PROCEDURE", "FUNCTION",
"TRIGGER", "EXEC", "EXECUTE", "SCHEMA", nullptr
};
std::string upper = value;
std::transform(upper.begin(), upper.end(), upper.begin(),
[](unsigned char c) { return std::toupper(c); });
for (const char** kw = keywords; *kw != nullptr; ++kw) {
if (upper == *kw) {
return true;
}
}
return false;
}
} // namespace dbal::security

View File

@@ -0,0 +1,20 @@
#pragma once
/**
* @file generate_nonce.hpp
* @brief Secure nonce generation for replay attack prevention
*/
#include <string>
#include "secure_random_hex.hpp"
namespace dbal::security {
/**
* Generate a secure nonce (32 hex chars = 128 bits)
* @return Cryptographically random nonce
*/
inline std::string generate_nonce() {
return secure_random_hex(16);
}
} // namespace dbal::security

View File

@@ -0,0 +1,20 @@
#pragma once
/**
* @file generate_request_id.hpp
* @brief Secure request ID generation
*/
#include <string>
#include "secure_random_hex.hpp"
namespace dbal::security {
/**
* Generate a secure request ID (32 hex chars = 128 bits)
* @return Cryptographically random request ID
*/
inline std::string generate_request_id() {
return secure_random_hex(16);
}
} // namespace dbal::security

View File

@@ -0,0 +1,20 @@
#pragma once
/**
* @file generate_token.hpp
* @brief Secure token generation
*/
#include <string>
#include "secure_random_hex.hpp"
namespace dbal::security {
/**
* Generate a secure token (64 hex chars = 256 bits)
* @return Cryptographically random token
*/
inline std::string generate_token() {
return secure_random_hex(32);
}
} // namespace dbal::security

View File

@@ -0,0 +1,47 @@
#pragma once
/**
* @file hmac_sha256.hpp
* @brief HMAC-SHA256 signature computation
*/
#include <string>
#include <openssl/hmac.h>
#include <openssl/evp.h>
namespace dbal::security {
/**
* Compute HMAC-SHA256 signature
* @param key Secret key bytes
* @param key_len Length of key
* @param data Data to sign
* @return Hex-encoded signature
*/
inline std::string hmac_sha256(
const unsigned char* key,
size_t key_len,
const std::string& data
) {
unsigned char result[EVP_MAX_MD_SIZE];
unsigned int result_len = 0;
HMAC(
EVP_sha256(),
key, static_cast<int>(key_len),
reinterpret_cast<const unsigned char*>(data.c_str()), data.size(),
result, &result_len
);
std::string hex;
hex.reserve(result_len * 2);
static const char hex_chars[] = "0123456789abcdef";
for (unsigned int i = 0; i < result_len; ++i) {
hex += hex_chars[(result[i] >> 4) & 0x0F];
hex += hex_chars[result[i] & 0x0F];
}
return hex;
}
} // namespace dbal::security

View File

@@ -0,0 +1,33 @@
#pragma once
/**
* @file is_safe_filename.hpp
* @brief Filename safety validation
*/
#include <string>
namespace dbal::security {
/**
* Check if a filename is safe (no path separators, no special names)
* @param filename Filename to validate
* @return true if safe
*/
inline bool is_safe_filename(const std::string& filename) {
if (filename.empty()) return false;
if (filename.find('/') != std::string::npos) return false;
if (filename.find('\\') != std::string::npos) return false;
if (filename == "." || filename == "..") return false;
if (filename.find('\0') != std::string::npos) return false;
for (char c : filename) {
if (static_cast<unsigned char>(c) < 32) return false;
}
return true;
}
} // namespace dbal::security

View File

@@ -0,0 +1,39 @@
#pragma once
/**
* @file is_valid_identifier.hpp
* @brief Database identifier validation
*/
#include <string>
#include <cctype>
namespace dbal::security {
/**
* Validate a string identifier (table names, column names, etc.)
* Only allows: a-z, A-Z, 0-9, underscore. Must start with letter/underscore.
*
* @param identifier String to validate
* @param max_length Maximum allowed length (default 64)
* @return true if valid
*/
inline bool is_valid_identifier(const std::string& identifier, size_t max_length = 64) {
if (identifier.empty() || identifier.size() > max_length) {
return false;
}
char first = identifier[0];
if (!std::isalpha(static_cast<unsigned char>(first)) && first != '_') {
return false;
}
for (char c : identifier) {
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
return false;
}
}
return true;
}
} // namespace dbal::security

View File

@@ -0,0 +1,38 @@
#pragma once
/**
* @file is_valid_uuid.hpp
* @brief UUID v4 format validation
*/
#include <string>
#include <cctype>
namespace dbal::security {
/**
* Validate UUID v4 format
* @param uuid String to validate
* @return true if valid UUID v4
*/
inline bool is_valid_uuid(const std::string& uuid) {
if (uuid.size() != 36) return false;
for (size_t i = 0; i < uuid.size(); ++i) {
char c = uuid[i];
if (i == 8 || i == 13 || i == 18 || i == 23) {
if (c != '-') return false;
} else if (i == 14) {
if (c != '4') return false;
} else if (i == 19) {
c = std::tolower(static_cast<unsigned char>(c));
if (c != '8' && c != '9' && c != 'a' && c != 'b') return false;
} else {
if (!std::isxdigit(static_cast<unsigned char>(c))) return false;
}
}
return true;
}
} // namespace dbal::security

View File

@@ -0,0 +1,39 @@
#pragma once
/**
* @file sanitize_string.hpp
* @brief String sanitization utility
*/
#include <string>
namespace dbal::security {
/**
* Sanitize string by removing/replacing dangerous characters
* @param input Input string
* @param allow_newlines Whether to allow newlines
* @return Sanitized string
*/
inline std::string sanitize_string(const std::string& input, bool allow_newlines = false) {
std::string result;
result.reserve(input.size());
for (char c : input) {
unsigned char uc = static_cast<unsigned char>(c);
if (c == '\0') continue;
if (uc < 32) {
if (allow_newlines && (c == '\n' || c == '\r' || c == '\t')) {
result += c;
}
continue;
}
result += c;
}
return result;
}
} // namespace dbal::security

View File

@@ -0,0 +1,46 @@
#pragma once
/**
* @file secure_random_bytes.hpp
* @brief Cryptographically secure random byte generation
*/
#include <cstddef>
#include <stdexcept>
#ifdef _WIN32
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
#else
#include <fstream>
#endif
namespace dbal::security {
/**
* Generate cryptographically secure random bytes
* @param buffer Destination buffer
* @param size Number of bytes to generate
* @throws std::runtime_error on failure
*/
inline void secure_random_bytes(unsigned char* buffer, size_t size) {
#ifdef _WIN32
NTSTATUS status = BCryptGenRandom(
nullptr, buffer, static_cast<ULONG>(size), BCRYPT_USE_SYSTEM_PREFERRED_RNG
);
if (!BCRYPT_SUCCESS(status)) {
throw std::runtime_error("BCryptGenRandom failed");
}
#else
std::ifstream urandom("/dev/urandom", std::ios::binary);
if (!urandom) {
throw std::runtime_error("Failed to open /dev/urandom");
}
urandom.read(reinterpret_cast<char*>(buffer), static_cast<std::streamsize>(size));
if (!urandom) {
throw std::runtime_error("Failed to read from /dev/urandom");
}
#endif
}
} // namespace dbal::security

View File

@@ -0,0 +1,34 @@
#pragma once
/**
* @file secure_random_hex.hpp
* @brief Secure random hex string generation
*/
#include <string>
#include <vector>
#include "secure_random_bytes.hpp"
namespace dbal::security {
/**
* Generate a secure random hex string
* @param bytes Number of random bytes (output will be 2x this length)
* @return Hex-encoded random string
*/
inline std::string secure_random_hex(size_t bytes) {
std::vector<unsigned char> buffer(bytes);
secure_random_bytes(buffer.data(), bytes);
static const char hex_chars[] = "0123456789abcdef";
std::string result;
result.reserve(bytes * 2);
for (unsigned char b : buffer) {
result += hex_chars[(b >> 4) & 0x0F];
result += hex_chars[b & 0x0F];
}
return result;
}
} // namespace dbal::security

View File

@@ -0,0 +1,30 @@
#pragma once
/**
* @file timing_safe_equal.hpp
* @brief Timing-safe string comparison to prevent timing attacks
*/
#include <string>
namespace dbal::security {
/**
* Timing-safe string comparison (prevents timing attacks)
* @param a First string
* @param b Second string
* @return true if equal
*/
inline bool timing_safe_equal(const std::string& a, const std::string& b) {
if (a.size() != b.size()) {
return false;
}
volatile unsigned char result = 0;
for (size_t i = 0; i < a.size(); ++i) {
result |= static_cast<unsigned char>(a[i]) ^ static_cast<unsigned char>(b[i]);
}
return result == 0;
}
} // namespace dbal::security

View File

@@ -0,0 +1,38 @@
#pragma once
/**
* @file validate_length.hpp
* @brief String length validation
*/
#include <string>
#include <stdexcept>
namespace dbal::security {
/**
* Validate string length within bounds
* @param value String to validate
* @param min_len Minimum length
* @param max_len Maximum length
* @param field_name Field name for error messages
* @throws std::runtime_error if validation fails
*/
inline void validate_length(
const std::string& value,
size_t min_len,
size_t max_len,
const char* field_name = "value"
) {
if (value.size() < min_len) {
throw std::runtime_error(
std::string(field_name) + " too short (min " + std::to_string(min_len) + ")"
);
}
if (value.size() > max_len) {
throw std::runtime_error(
std::string(field_name) + " too long (max " + std::to_string(max_len) + ")"
);
}
}
} // namespace dbal::security

View File

@@ -0,0 +1,63 @@
#pragma once
/**
* @file validate_path.hpp
* @brief Secure path validation to prevent directory traversal
*/
#include <string>
#include <filesystem>
#include <stdexcept>
namespace dbal::security {
/**
* Validate and resolve a path safely within a base directory
* Prevents: ../, encoded traversal, symlink escapes, null bytes
*
* @param base_path Allowed base directory (must be absolute)
* @param user_path User-supplied relative path
* @return Resolved safe absolute path
* @throws std::runtime_error if path is invalid or escapes base
*/
inline std::string validate_path(
const std::string& base_path,
const std::string& user_path
) {
namespace fs = std::filesystem;
if (user_path.find('\0') != std::string::npos) {
throw std::runtime_error("Path contains null byte");
}
if (user_path.find("..") != std::string::npos) {
throw std::runtime_error("Path contains traversal sequence");
}
if (!user_path.empty() && (user_path[0] == '/' || user_path[0] == '\\')) {
throw std::runtime_error("Absolute paths not allowed");
}
if (user_path.find('%') != std::string::npos) {
throw std::runtime_error("Encoded characters not allowed");
}
fs::path base = fs::canonical(base_path);
fs::path combined = base / user_path;
fs::path resolved = fs::weakly_canonical(combined);
std::string base_str = base.string();
std::string resolved_str = resolved.string();
if (!base_str.empty() && base_str.back() != fs::path::preferred_separator) {
base_str += fs::path::preferred_separator;
}
if (resolved_str.compare(0, base_str.size(), base_str) != 0 &&
resolved_str != base.string()) {
throw std::runtime_error("Path escapes allowed directory");
}
return resolved_str;
}
} // namespace dbal::security

View File

@@ -0,0 +1,11 @@
import { NextResponse } from 'next/server'
import { PERMISSION_LEVELS } from '@/app/levels/levels-data'
export async function GET() {
return NextResponse.json({
status: 'ok',
levelCount: PERMISSION_LEVELS.length,
timestamp: new Date().toISOString(),
})
}