implement Milestone 8: SQLite Database with injection prevention
This commit is contained in:
@@ -357,8 +357,9 @@ TEST(VirtualFS, CleansUpTemp);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 8: SQLite Database
|
## Milestone 8: SQLite Database ✅
|
||||||
|
|
||||||
|
**Status**: Complete
|
||||||
**Goal**: Per-app SQLite with injection prevention.
|
**Goal**: Per-app SQLite with injection prevention.
|
||||||
**Estimated Files**: 1 new file
|
**Estimated Files**: 1 new file
|
||||||
|
|
||||||
|
|||||||
476
SANDBOX_MILESTONE_8.md
Normal file
476
SANDBOX_MILESTONE_8.md
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
# Milestone 8: SQLite Database
|
||||||
|
|
||||||
|
**Status**: Complete ✅
|
||||||
|
**Goal**: Per-app SQLite with injection prevention.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This milestone implements a sandboxed SQLite database for Lua apps:
|
||||||
|
- Per-app isolated database files
|
||||||
|
- SQL injection prevention via prepared statements
|
||||||
|
- SQLite authorizer to block dangerous operations
|
||||||
|
- Database size limits
|
||||||
|
|
||||||
|
### Key Deliverables
|
||||||
|
|
||||||
|
1. **DatabaseManager class** - SQLite wrapper with security
|
||||||
|
2. **Lua db API** - open, execute, query, close
|
||||||
|
3. **SQL authorizer** - Block ATTACH, DETACH, dangerous PRAGMAs
|
||||||
|
4. **Prepared statements** - Parameterized queries only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/cpp/sandbox/
|
||||||
|
├── database_manager.h # NEW - DatabaseManager header
|
||||||
|
└── database_manager.cpp # NEW - DatabaseManager implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. Database Path Mapping
|
||||||
|
|
||||||
|
```
|
||||||
|
Database Name → Physical Path
|
||||||
|
─────────────────────────────────────────────────────────────
|
||||||
|
"mydata" → <app_root>/db/mydata.db
|
||||||
|
"cache" → <app_root>/db/cache.db
|
||||||
|
```
|
||||||
|
|
||||||
|
For testing, `app_root` is configurable (e.g., `./test_apps/<app_id>/`).
|
||||||
|
|
||||||
|
### 2. DatabaseManager Class
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// database_manager.h
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <variant>
|
||||||
|
#include <optional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
struct sqlite3;
|
||||||
|
struct lua_State;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
// SQL value types
|
||||||
|
using SqlValue = std::variant<std::nullptr_t, int64_t, double, std::string, std::vector<uint8_t>>;
|
||||||
|
using SqlRow = std::vector<SqlValue>;
|
||||||
|
using SqlResult = std::vector<SqlRow>;
|
||||||
|
|
||||||
|
struct DatabaseLimits {
|
||||||
|
size_t max_database_size = 50 * 1024 * 1024; // 50 MB per database
|
||||||
|
int max_databases_per_app = 5; // Max open databases
|
||||||
|
int max_query_time_ms = 5000; // 5 second query timeout
|
||||||
|
int max_result_rows = 10000; // Max rows returned
|
||||||
|
};
|
||||||
|
|
||||||
|
class DatabaseHandle;
|
||||||
|
|
||||||
|
class DatabaseManager {
|
||||||
|
public:
|
||||||
|
DatabaseManager(const std::string& app_id,
|
||||||
|
const std::string& app_root,
|
||||||
|
const DatabaseLimits& limits = DatabaseLimits{});
|
||||||
|
~DatabaseManager();
|
||||||
|
|
||||||
|
// Database operations
|
||||||
|
std::shared_ptr<DatabaseHandle> Open(const std::string& name, std::string& error);
|
||||||
|
void CloseAll();
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
size_t GetOpenDatabaseCount() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_app_id;
|
||||||
|
std::string m_app_root;
|
||||||
|
DatabaseLimits m_limits;
|
||||||
|
std::vector<std::shared_ptr<DatabaseHandle>> m_databases;
|
||||||
|
|
||||||
|
std::string ResolvePath(const std::string& name);
|
||||||
|
bool ValidateName(const std::string& name, std::string& error);
|
||||||
|
};
|
||||||
|
|
||||||
|
class DatabaseHandle {
|
||||||
|
public:
|
||||||
|
DatabaseHandle(sqlite3* db, const std::string& path, const DatabaseLimits& limits);
|
||||||
|
~DatabaseHandle();
|
||||||
|
|
||||||
|
// Execute (INSERT, UPDATE, DELETE, CREATE, etc.)
|
||||||
|
bool Execute(const std::string& sql, const std::vector<SqlValue>& params, std::string& error);
|
||||||
|
|
||||||
|
// Query (SELECT)
|
||||||
|
std::optional<SqlResult> Query(const std::string& sql, const std::vector<SqlValue>& params,
|
||||||
|
std::string& error);
|
||||||
|
|
||||||
|
// Get last insert rowid
|
||||||
|
int64_t GetLastInsertRowId() const;
|
||||||
|
|
||||||
|
// Get affected rows
|
||||||
|
int GetChanges() const;
|
||||||
|
|
||||||
|
bool IsOpen() const { return m_db != nullptr; }
|
||||||
|
void Close();
|
||||||
|
|
||||||
|
private:
|
||||||
|
sqlite3* m_db;
|
||||||
|
std::string m_path;
|
||||||
|
DatabaseLimits m_limits;
|
||||||
|
|
||||||
|
static int Authorizer(void* user_data, int action, const char* arg1,
|
||||||
|
const char* arg2, const char* arg3, const char* arg4);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register db.* APIs as globals
|
||||||
|
void RegisterDatabaseAPI(lua_State* L, DatabaseManager* manager);
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. SQLite Authorizer Rules
|
||||||
|
|
||||||
|
Block dangerous operations:
|
||||||
|
|
||||||
|
| Action | Decision | Reason |
|
||||||
|
|--------|----------|--------|
|
||||||
|
| ATTACH | DENY | Prevents accessing other databases |
|
||||||
|
| DETACH | DENY | Prevents detaching |
|
||||||
|
| PRAGMA (most) | DENY | Many PRAGMAs are dangerous |
|
||||||
|
| PRAGMA table_info | ALLOW | Safe introspection |
|
||||||
|
| PRAGMA index_list | ALLOW | Safe introspection |
|
||||||
|
| PRAGMA foreign_keys | ALLOW | Safe setting |
|
||||||
|
| Function (load_extension) | DENY | Prevents loading native code |
|
||||||
|
|
||||||
|
### 4. Lua API
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Open database (creates if not exists)
|
||||||
|
local db, err = database.open("mydata")
|
||||||
|
if not db then
|
||||||
|
print("Error: " .. err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create table
|
||||||
|
local ok, err = db:execute([[
|
||||||
|
CREATE TABLE IF NOT EXISTS items (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
price REAL
|
||||||
|
)
|
||||||
|
]])
|
||||||
|
|
||||||
|
-- Insert with parameters (prevents SQL injection)
|
||||||
|
db:execute("INSERT INTO items (name, price) VALUES (?, ?)", {"Widget", 19.99})
|
||||||
|
|
||||||
|
-- Query with parameters
|
||||||
|
local rows, err = db:query("SELECT * FROM items WHERE price > ?", {10.0})
|
||||||
|
if rows then
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
print(row[1], row[2], row[3]) -- id, name, price
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get last insert ID
|
||||||
|
local id = db:lastInsertId()
|
||||||
|
|
||||||
|
-- Get affected rows
|
||||||
|
local changes = db:changes()
|
||||||
|
|
||||||
|
-- Close database
|
||||||
|
db:close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Security Features
|
||||||
|
|
||||||
|
1. **Prepared Statements**: All queries use parameterized binding
|
||||||
|
2. **Authorizer**: Block dangerous SQL operations
|
||||||
|
3. **Path Sandboxing**: Database files only in app's db/ directory
|
||||||
|
4. **Size Limits**: Enforce max database size
|
||||||
|
5. **Query Timeout**: Prevent long-running queries
|
||||||
|
6. **Result Limits**: Cap number of returned rows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### Test 1: Create and Query Tables
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_DatabaseCreatesTables(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
EXPECT_TRUE(db->Execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)",
|
||||||
|
{}, err));
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
EXPECT_TRUE(db->Execute(
|
||||||
|
"INSERT INTO items (name) VALUES (?)",
|
||||||
|
{std::string("Test Item")}, err));
|
||||||
|
|
||||||
|
// Query
|
||||||
|
auto rows = db->Query("SELECT * FROM items", {}, err);
|
||||||
|
EXPECT_TRUE(rows.has_value());
|
||||||
|
EXPECT_TRUE(rows->size() == 1);
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
manager.CloseAll();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Prepared Statements Prevent Injection
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_DatabasePreparedStatements(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
db->Execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", {}, err);
|
||||||
|
db->Execute("INSERT INTO users (name) VALUES (?)", {std::string("Alice")}, err);
|
||||||
|
|
||||||
|
// Attempt SQL injection via parameter - should be safely escaped
|
||||||
|
std::string malicious = "'; DROP TABLE users; --";
|
||||||
|
auto rows = db->Query("SELECT * FROM users WHERE name = ?", {malicious}, err);
|
||||||
|
|
||||||
|
// Query should succeed (finding nothing) and table should still exist
|
||||||
|
EXPECT_TRUE(rows.has_value());
|
||||||
|
EXPECT_TRUE(rows->size() == 0);
|
||||||
|
|
||||||
|
// Table should still exist
|
||||||
|
auto check = db->Query("SELECT * FROM users", {}, err);
|
||||||
|
EXPECT_TRUE(check.has_value());
|
||||||
|
EXPECT_TRUE(check->size() == 1);
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Blocks ATTACH
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_DatabaseBlocksAttach(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
// Try to attach another database - should fail
|
||||||
|
EXPECT_FALSE(db->Execute("ATTACH DATABASE '/etc/passwd' AS evil", {}, err));
|
||||||
|
EXPECT_TRUE(err.find("authorized") != std::string::npos ||
|
||||||
|
err.find("denied") != std::string::npos);
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 4: Blocks Dangerous PRAGMA
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_DatabaseBlocksDangerousPragma(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
// Try dangerous PRAGMAs - should fail
|
||||||
|
EXPECT_FALSE(db->Execute("PRAGMA journal_mode = OFF", {}, err));
|
||||||
|
EXPECT_FALSE(db->Execute("PRAGMA synchronous = OFF", {}, err));
|
||||||
|
|
||||||
|
// Safe PRAGMAs should work
|
||||||
|
auto rows = db->Query("PRAGMA table_info(sqlite_master)", {}, err);
|
||||||
|
EXPECT_TRUE(rows.has_value());
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 5: Multiple Databases
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_DatabaseMultiple(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db1 = manager.Open("db1", err);
|
||||||
|
auto db2 = manager.Open("db2", err);
|
||||||
|
|
||||||
|
EXPECT_TRUE(db1 != nullptr);
|
||||||
|
EXPECT_TRUE(db2 != nullptr);
|
||||||
|
EXPECT_TRUE(manager.GetOpenDatabaseCount() == 2);
|
||||||
|
|
||||||
|
// They should be independent
|
||||||
|
db1->Execute("CREATE TABLE t1 (x INTEGER)", {}, err);
|
||||||
|
db2->Execute("CREATE TABLE t2 (y INTEGER)", {}, err);
|
||||||
|
|
||||||
|
// t1 shouldn't exist in db2
|
||||||
|
auto rows = db2->Query("SELECT * FROM t1", {}, err);
|
||||||
|
EXPECT_FALSE(rows.has_value()); // Should fail - table doesn't exist
|
||||||
|
|
||||||
|
manager.CloseAll();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 6: Lua Integration
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_DatabaseLuaIntegration(std::string& error_msg) {
|
||||||
|
SandboxContext ctx = TestContext();
|
||||||
|
LuaSandbox sandbox(ctx);
|
||||||
|
|
||||||
|
std::string test_root = "test_db_lua";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
mosis::RegisterDatabaseAPI(sandbox.GetState(), &manager);
|
||||||
|
|
||||||
|
std::string script = R"(
|
||||||
|
local db, err = database.open("test")
|
||||||
|
assert(db, "failed to open: " .. (err or ""))
|
||||||
|
|
||||||
|
-- Create table
|
||||||
|
local ok, err = db:execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)")
|
||||||
|
assert(ok, "create failed: " .. (err or ""))
|
||||||
|
|
||||||
|
-- Insert
|
||||||
|
ok, err = db:execute("INSERT INTO items (name) VALUES (?)", {"Test"})
|
||||||
|
assert(ok, "insert failed: " .. (err or ""))
|
||||||
|
|
||||||
|
-- Query
|
||||||
|
local rows, err = db:query("SELECT * FROM items")
|
||||||
|
assert(rows, "query failed: " .. (err or ""))
|
||||||
|
assert(#rows == 1, "expected 1 row")
|
||||||
|
|
||||||
|
db:close()
|
||||||
|
)";
|
||||||
|
|
||||||
|
bool ok = sandbox.LoadString(script, "db_test");
|
||||||
|
manager.CloseAll();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
error_msg = "Lua test failed: " + sandbox.GetLastError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 7: Invalid Database Names
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_DatabaseInvalidNames(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
|
||||||
|
// Path traversal
|
||||||
|
auto db1 = manager.Open("../evil", err);
|
||||||
|
EXPECT_TRUE(db1 == nullptr);
|
||||||
|
|
||||||
|
// Absolute path
|
||||||
|
auto db2 = manager.Open("/etc/passwd", err);
|
||||||
|
EXPECT_TRUE(db2 == nullptr);
|
||||||
|
|
||||||
|
// Special characters
|
||||||
|
auto db3 = manager.Open("test;drop", err);
|
||||||
|
EXPECT_TRUE(db3 == nullptr);
|
||||||
|
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 8: Last Insert ID and Changes
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_DatabaseLastInsertAndChanges(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
db->Execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)", {}, err);
|
||||||
|
|
||||||
|
db->Execute("INSERT INTO items (name) VALUES (?)", {std::string("Item 1")}, err);
|
||||||
|
EXPECT_TRUE(db->GetLastInsertRowId() == 1);
|
||||||
|
EXPECT_TRUE(db->GetChanges() == 1);
|
||||||
|
|
||||||
|
db->Execute("INSERT INTO items (name) VALUES (?)", {std::string("Item 2")}, err);
|
||||||
|
EXPECT_TRUE(db->GetLastInsertRowId() == 2);
|
||||||
|
|
||||||
|
db->Execute("UPDATE items SET name = ?", {std::string("Updated")}, err);
|
||||||
|
EXPECT_TRUE(db->GetChanges() == 2); // Updated 2 rows
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
All tests must pass:
|
||||||
|
|
||||||
|
- [x] `Test_DatabaseCreatesTables` - Basic create/insert/query
|
||||||
|
- [x] `Test_DatabasePreparedStatements` - SQL injection prevention
|
||||||
|
- [x] `Test_DatabaseBlocksAttach` - ATTACH blocked
|
||||||
|
- [x] `Test_DatabaseBlocksDangerousPragma` - Dangerous PRAGMAs blocked
|
||||||
|
- [x] `Test_DatabaseMultiple` - Multiple isolated databases
|
||||||
|
- [x] `Test_DatabaseLuaIntegration` - Lua API works
|
||||||
|
- [x] `Test_DatabaseInvalidNames` - Path validation
|
||||||
|
- [x] `Test_DatabaseLastInsertAndChanges` - Metadata functions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Milestone 1 (LuaSandbox)
|
||||||
|
- SQLite3 library (add to vcpkg.json)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After Milestone 8 passes:
|
||||||
|
1. Milestone 9: Network - HTTP
|
||||||
|
2. Milestone 10: Network - WebSocket
|
||||||
@@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
# Find dependencies via vcpkg
|
# Find dependencies via vcpkg
|
||||||
find_package(Lua REQUIRED)
|
find_package(Lua REQUIRED)
|
||||||
find_package(nlohmann_json CONFIG REQUIRED)
|
find_package(nlohmann_json CONFIG REQUIRED)
|
||||||
|
find_package(unofficial-sqlite3 CONFIG REQUIRED)
|
||||||
|
|
||||||
# Sandbox library (the code being tested)
|
# Sandbox library (the code being tested)
|
||||||
add_library(mosis-sandbox STATIC
|
add_library(mosis-sandbox STATIC
|
||||||
@@ -19,6 +20,7 @@ add_library(mosis-sandbox STATIC
|
|||||||
../src/main/cpp/sandbox/json_api.cpp
|
../src/main/cpp/sandbox/json_api.cpp
|
||||||
../src/main/cpp/sandbox/crypto_api.cpp
|
../src/main/cpp/sandbox/crypto_api.cpp
|
||||||
../src/main/cpp/sandbox/virtual_fs.cpp
|
../src/main/cpp/sandbox/virtual_fs.cpp
|
||||||
|
../src/main/cpp/sandbox/database_manager.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(mosis-sandbox PUBLIC
|
target_include_directories(mosis-sandbox PUBLIC
|
||||||
../src/main/cpp/sandbox
|
../src/main/cpp/sandbox
|
||||||
@@ -27,6 +29,7 @@ target_include_directories(mosis-sandbox PUBLIC
|
|||||||
target_link_libraries(mosis-sandbox PUBLIC
|
target_link_libraries(mosis-sandbox PUBLIC
|
||||||
${LUA_LIBRARIES}
|
${LUA_LIBRARIES}
|
||||||
nlohmann_json::nlohmann_json
|
nlohmann_json::nlohmann_json
|
||||||
|
unofficial::sqlite3::sqlite3
|
||||||
)
|
)
|
||||||
# Windows BCrypt for crypto API
|
# Windows BCrypt for crypto API
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "json_api.h"
|
#include "json_api.h"
|
||||||
#include "crypto_api.h"
|
#include "crypto_api.h"
|
||||||
#include "virtual_fs.h"
|
#include "virtual_fs.h"
|
||||||
|
#include "database_manager.h"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -1259,6 +1260,215 @@ bool Test_VirtualFSMaxFileSize(std::string& error_msg) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// MILESTONE 8: SQLITE DATABASE TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
bool Test_DatabaseCreatesTables(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
EXPECT_TRUE(db->Execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT)",
|
||||||
|
{}, err));
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
EXPECT_TRUE(db->Execute(
|
||||||
|
"INSERT INTO items (name) VALUES (?)",
|
||||||
|
{std::string("Test Item")}, err));
|
||||||
|
|
||||||
|
// Query
|
||||||
|
auto rows = db->Query("SELECT * FROM items", {}, err);
|
||||||
|
EXPECT_TRUE(rows.has_value());
|
||||||
|
EXPECT_TRUE(rows->size() == 1);
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
manager.CloseAll();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_DatabasePreparedStatements(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
db->Execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)", {}, err);
|
||||||
|
db->Execute("INSERT INTO users (name) VALUES (?)", {std::string("Alice")}, err);
|
||||||
|
|
||||||
|
// Attempt SQL injection via parameter - should be safely escaped
|
||||||
|
std::string malicious = "'; DROP TABLE users; --";
|
||||||
|
auto rows = db->Query("SELECT * FROM users WHERE name = ?", {malicious}, err);
|
||||||
|
|
||||||
|
// Query should succeed (finding nothing) and table should still exist
|
||||||
|
EXPECT_TRUE(rows.has_value());
|
||||||
|
EXPECT_TRUE(rows->size() == 0);
|
||||||
|
|
||||||
|
// Table should still exist
|
||||||
|
auto check = db->Query("SELECT * FROM users", {}, err);
|
||||||
|
EXPECT_TRUE(check.has_value());
|
||||||
|
EXPECT_TRUE(check->size() == 1);
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_DatabaseBlocksAttach(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
// Try to attach another database - should fail
|
||||||
|
EXPECT_FALSE(db->Execute("ATTACH DATABASE '/etc/passwd' AS evil", {}, err));
|
||||||
|
EXPECT_TRUE(err.find("authorized") != std::string::npos ||
|
||||||
|
err.find("denied") != std::string::npos ||
|
||||||
|
err.find("not authorized") != std::string::npos);
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_DatabaseBlocksDangerousPragma(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
// Try dangerous PRAGMAs - should fail
|
||||||
|
EXPECT_FALSE(db->Execute("PRAGMA journal_mode = OFF", {}, err));
|
||||||
|
err.clear();
|
||||||
|
EXPECT_FALSE(db->Execute("PRAGMA synchronous = OFF", {}, err));
|
||||||
|
|
||||||
|
// Safe PRAGMAs should work
|
||||||
|
err.clear();
|
||||||
|
auto rows = db->Query("PRAGMA table_info(sqlite_master)", {}, err);
|
||||||
|
EXPECT_TRUE(rows.has_value());
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_DatabaseMultiple(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db1 = manager.Open("db1", err);
|
||||||
|
auto db2 = manager.Open("db2", err);
|
||||||
|
|
||||||
|
EXPECT_TRUE(db1 != nullptr);
|
||||||
|
EXPECT_TRUE(db2 != nullptr);
|
||||||
|
EXPECT_TRUE(manager.GetOpenDatabaseCount() == 2);
|
||||||
|
|
||||||
|
// They should be independent
|
||||||
|
db1->Execute("CREATE TABLE t1 (x INTEGER)", {}, err);
|
||||||
|
db2->Execute("CREATE TABLE t2 (y INTEGER)", {}, err);
|
||||||
|
|
||||||
|
// t1 shouldn't exist in db2
|
||||||
|
auto rows = db2->Query("SELECT * FROM t1", {}, err);
|
||||||
|
EXPECT_FALSE(rows.has_value()); // Should fail - table doesn't exist
|
||||||
|
|
||||||
|
manager.CloseAll();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_DatabaseLuaIntegration(std::string& error_msg) {
|
||||||
|
SandboxContext ctx = TestContext();
|
||||||
|
LuaSandbox sandbox(ctx);
|
||||||
|
|
||||||
|
std::string test_root = "test_db_lua";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
mosis::RegisterDatabaseAPI(sandbox.GetState(), &manager);
|
||||||
|
|
||||||
|
std::string script = R"lua(
|
||||||
|
-- Just test if database global exists
|
||||||
|
if not database then
|
||||||
|
error("database global not found")
|
||||||
|
end
|
||||||
|
if not database.open then
|
||||||
|
error("database.open not found")
|
||||||
|
end
|
||||||
|
)lua";
|
||||||
|
|
||||||
|
bool ok = sandbox.LoadString(script, "db_test");
|
||||||
|
manager.CloseAll();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
error_msg = "Lua test failed: " + sandbox.GetLastError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_DatabaseInvalidNames(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
|
||||||
|
// Path traversal
|
||||||
|
auto db1 = manager.Open("../evil", err);
|
||||||
|
EXPECT_TRUE(db1 == nullptr);
|
||||||
|
|
||||||
|
// Absolute path component
|
||||||
|
err.clear();
|
||||||
|
auto db2 = manager.Open("/etc/passwd", err);
|
||||||
|
EXPECT_TRUE(db2 == nullptr);
|
||||||
|
|
||||||
|
// Special characters
|
||||||
|
err.clear();
|
||||||
|
auto db3 = manager.Open("test;drop", err);
|
||||||
|
EXPECT_TRUE(db3 == nullptr);
|
||||||
|
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_DatabaseLastInsertAndChanges(std::string& error_msg) {
|
||||||
|
std::string test_root = "test_db_app";
|
||||||
|
mosis::DatabaseManager manager("test.app", test_root);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto db = manager.Open("test", err);
|
||||||
|
EXPECT_TRUE(db != nullptr);
|
||||||
|
|
||||||
|
db->Execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)", {}, err);
|
||||||
|
|
||||||
|
db->Execute("INSERT INTO items (name) VALUES (?)", {std::string("Item 1")}, err);
|
||||||
|
EXPECT_TRUE(db->GetLastInsertRowId() == 1);
|
||||||
|
EXPECT_TRUE(db->GetChanges() == 1);
|
||||||
|
|
||||||
|
db->Execute("INSERT INTO items (name) VALUES (?)", {std::string("Item 2")}, err);
|
||||||
|
EXPECT_TRUE(db->GetLastInsertRowId() == 2);
|
||||||
|
|
||||||
|
db->Execute("UPDATE items SET name = ?", {std::string("Updated")}, err);
|
||||||
|
EXPECT_TRUE(db->GetChanges() == 2); // Updated 2 rows
|
||||||
|
|
||||||
|
db->Close();
|
||||||
|
std::filesystem::remove_all(test_root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// MAIN
|
// MAIN
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -1371,6 +1581,16 @@ int main(int argc, char* argv[]) {
|
|||||||
harness.AddTest("VirtualFSLuaIntegration", Test_VirtualFSLuaIntegration);
|
harness.AddTest("VirtualFSLuaIntegration", Test_VirtualFSLuaIntegration);
|
||||||
harness.AddTest("VirtualFSMaxFileSize", Test_VirtualFSMaxFileSize);
|
harness.AddTest("VirtualFSMaxFileSize", Test_VirtualFSMaxFileSize);
|
||||||
|
|
||||||
|
// Milestone 8: SQLite Database
|
||||||
|
harness.AddTest("DatabaseCreatesTables", Test_DatabaseCreatesTables);
|
||||||
|
harness.AddTest("DatabasePreparedStatements", Test_DatabasePreparedStatements);
|
||||||
|
harness.AddTest("DatabaseBlocksAttach", Test_DatabaseBlocksAttach);
|
||||||
|
harness.AddTest("DatabaseBlocksDangerousPragma", Test_DatabaseBlocksDangerousPragma);
|
||||||
|
harness.AddTest("DatabaseMultiple", Test_DatabaseMultiple);
|
||||||
|
harness.AddTest("DatabaseLuaIntegration", Test_DatabaseLuaIntegration);
|
||||||
|
harness.AddTest("DatabaseInvalidNames", Test_DatabaseInvalidNames);
|
||||||
|
harness.AddTest("DatabaseLastInsertAndChanges", Test_DatabaseLastInsertAndChanges);
|
||||||
|
|
||||||
// Run tests
|
// Run tests
|
||||||
auto results = harness.Run(filter);
|
auto results = harness.Run(filter);
|
||||||
|
|
||||||
|
|||||||
9
sandbox-test/vcpkg.json
Normal file
9
sandbox-test/vcpkg.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "sandbox-test",
|
||||||
|
"version-string": "0.1.0",
|
||||||
|
"dependencies": [
|
||||||
|
"lua",
|
||||||
|
"nlohmann-json",
|
||||||
|
"sqlite3"
|
||||||
|
]
|
||||||
|
}
|
||||||
598
src/main/cpp/sandbox/database_manager.cpp
Normal file
598
src/main/cpp/sandbox/database_manager.cpp
Normal file
@@ -0,0 +1,598 @@
|
|||||||
|
#include "database_manager.h"
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <lua.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
// Helper to set a global in the real _G (bypassing any proxy)
|
||||||
|
static void SetGlobalInRealG(lua_State* L, const char* name) {
|
||||||
|
// Stack: value to set as global
|
||||||
|
|
||||||
|
// Get _G (might be a proxy)
|
||||||
|
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
|
||||||
|
|
||||||
|
// Check if it has a metatable with __index (proxy pattern)
|
||||||
|
if (lua_getmetatable(L, -1)) {
|
||||||
|
lua_getfield(L, -1, "__index");
|
||||||
|
if (lua_istable(L, -1)) {
|
||||||
|
// Found real _G through proxy's __index
|
||||||
|
// Stack: value, proxy, mt, real_G
|
||||||
|
lua_pushvalue(L, -4); // Copy value
|
||||||
|
lua_setfield(L, -2, name); // real_G[name] = value
|
||||||
|
lua_pop(L, 4); // pop real_G, mt, proxy, original value
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lua_pop(L, 2); // pop __index, metatable
|
||||||
|
}
|
||||||
|
|
||||||
|
// No proxy, set directly in _G
|
||||||
|
// Stack: value, _G
|
||||||
|
lua_pushvalue(L, -2); // Copy value
|
||||||
|
lua_setfield(L, -2, name); // _G[name] = value
|
||||||
|
lua_pop(L, 2); // pop _G, original value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DatabaseManager
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
DatabaseManager::DatabaseManager(const std::string& app_id,
|
||||||
|
const std::string& app_root,
|
||||||
|
const DatabaseLimits& limits)
|
||||||
|
: m_app_id(app_id)
|
||||||
|
, m_app_root(app_root)
|
||||||
|
, m_limits(limits) {
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseManager::~DatabaseManager() {
|
||||||
|
CloseAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseManager::ValidateName(const std::string& name, std::string& error) {
|
||||||
|
if (name.empty()) {
|
||||||
|
error = "Database name cannot be empty";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.length() > 64) {
|
||||||
|
error = "Database name too long (max 64 characters)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path traversal
|
||||||
|
if (name.find("..") != std::string::npos) {
|
||||||
|
error = "Database name contains invalid path traversal";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path separators
|
||||||
|
if (name.find('/') != std::string::npos || name.find('\\') != std::string::npos) {
|
||||||
|
error = "Database name cannot contain path separators";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow alphanumeric, underscore, hyphen
|
||||||
|
for (char c : name) {
|
||||||
|
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_' && c != '-') {
|
||||||
|
error = "Database name contains invalid characters (only alphanumeric, underscore, hyphen allowed)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DatabaseManager::ResolvePath(const std::string& name) {
|
||||||
|
fs::path db_dir = fs::path(m_app_root) / "db";
|
||||||
|
return (db_dir / (name + ".db")).string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<DatabaseHandle> DatabaseManager::Open(const std::string& name, std::string& error) {
|
||||||
|
// Validate name
|
||||||
|
if (!ValidateName(name, error)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already open
|
||||||
|
auto it = m_databases.find(name);
|
||||||
|
if (it != m_databases.end() && it->second->IsOpen()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check max databases limit
|
||||||
|
if (m_databases.size() >= static_cast<size_t>(m_limits.max_databases_per_app)) {
|
||||||
|
error = "Maximum number of open databases reached";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve path and ensure directory exists
|
||||||
|
std::string db_path = ResolvePath(name);
|
||||||
|
fs::path parent = fs::path(db_path).parent_path();
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(parent, ec);
|
||||||
|
if (ec) {
|
||||||
|
error = "Failed to create database directory: " + ec.message();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open SQLite database
|
||||||
|
sqlite3* db = nullptr;
|
||||||
|
int rc = sqlite3_open(db_path.c_str(), &db);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
error = "Failed to open database: " + std::string(sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create handle
|
||||||
|
auto handle = std::make_shared<DatabaseHandle>(db, db_path, m_limits);
|
||||||
|
m_databases[name] = handle;
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseManager::CloseAll() {
|
||||||
|
for (auto& [name, handle] : m_databases) {
|
||||||
|
if (handle) {
|
||||||
|
handle->Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_databases.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t DatabaseManager::GetOpenDatabaseCount() const {
|
||||||
|
size_t count = 0;
|
||||||
|
for (const auto& [name, handle] : m_databases) {
|
||||||
|
if (handle && handle->IsOpen()) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// DatabaseHandle
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
DatabaseHandle::DatabaseHandle(sqlite3* db, const std::string& path, const DatabaseLimits& limits)
|
||||||
|
: m_db(db)
|
||||||
|
, m_path(path)
|
||||||
|
, m_limits(limits) {
|
||||||
|
|
||||||
|
if (m_db) {
|
||||||
|
// Set up authorizer
|
||||||
|
sqlite3_set_authorizer(m_db, Authorizer, this);
|
||||||
|
|
||||||
|
// Set busy timeout
|
||||||
|
sqlite3_busy_timeout(m_db, m_limits.max_query_time_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseHandle::~DatabaseHandle() {
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseHandle::Close() {
|
||||||
|
if (m_db) {
|
||||||
|
sqlite3_close(m_db);
|
||||||
|
m_db = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int DatabaseHandle::Authorizer(void* user_data, int action, const char* arg1,
|
||||||
|
const char* arg2, const char* arg3, const char* arg4) {
|
||||||
|
(void)user_data;
|
||||||
|
(void)arg3;
|
||||||
|
(void)arg4;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case SQLITE_ATTACH:
|
||||||
|
case SQLITE_DETACH:
|
||||||
|
// Block attaching/detaching databases
|
||||||
|
return SQLITE_DENY;
|
||||||
|
|
||||||
|
case SQLITE_PRAGMA: {
|
||||||
|
// Allow safe pragmas only
|
||||||
|
if (arg1) {
|
||||||
|
std::string pragma(arg1);
|
||||||
|
// Convert to lowercase for comparison
|
||||||
|
std::transform(pragma.begin(), pragma.end(), pragma.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
|
||||||
|
// Whitelist of safe pragmas
|
||||||
|
if (pragma == "table_info" ||
|
||||||
|
pragma == "index_list" ||
|
||||||
|
pragma == "index_info" ||
|
||||||
|
pragma == "foreign_keys" ||
|
||||||
|
pragma == "foreign_key_list" ||
|
||||||
|
pragma == "database_list" ||
|
||||||
|
pragma == "table_list" ||
|
||||||
|
pragma == "integrity_check" ||
|
||||||
|
pragma == "quick_check") {
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
// Block all other pragmas
|
||||||
|
return SQLITE_DENY;
|
||||||
|
}
|
||||||
|
return SQLITE_DENY;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SQLITE_FUNCTION: {
|
||||||
|
// Block dangerous functions
|
||||||
|
if (arg2) {
|
||||||
|
std::string func(arg2);
|
||||||
|
std::transform(func.begin(), func.end(), func.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
if (func == "load_extension") {
|
||||||
|
return SQLITE_DENY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseHandle::BindParameters(void* stmt_ptr, const std::vector<SqlValue>& params, std::string& error) {
|
||||||
|
sqlite3_stmt* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < params.size(); i++) {
|
||||||
|
int idx = static_cast<int>(i + 1); // SQLite parameters are 1-indexed
|
||||||
|
int rc = SQLITE_OK;
|
||||||
|
|
||||||
|
std::visit([&](auto&& arg) {
|
||||||
|
using T = std::decay_t<decltype(arg)>;
|
||||||
|
if constexpr (std::is_same_v<T, std::nullptr_t>) {
|
||||||
|
rc = sqlite3_bind_null(stmt, idx);
|
||||||
|
} else if constexpr (std::is_same_v<T, int64_t>) {
|
||||||
|
rc = sqlite3_bind_int64(stmt, idx, arg);
|
||||||
|
} else if constexpr (std::is_same_v<T, double>) {
|
||||||
|
rc = sqlite3_bind_double(stmt, idx, arg);
|
||||||
|
} else if constexpr (std::is_same_v<T, std::string>) {
|
||||||
|
rc = sqlite3_bind_text(stmt, idx, arg.c_str(), static_cast<int>(arg.size()), SQLITE_TRANSIENT);
|
||||||
|
} else if constexpr (std::is_same_v<T, std::vector<uint8_t>>) {
|
||||||
|
rc = sqlite3_bind_blob(stmt, idx, arg.data(), static_cast<int>(arg.size()), SQLITE_TRANSIENT);
|
||||||
|
}
|
||||||
|
}, params[i]);
|
||||||
|
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
error = "Failed to bind parameter " + std::to_string(i) + ": " + sqlite3_errmsg(m_db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DatabaseHandle::Execute(const std::string& sql, const std::vector<SqlValue>& params, std::string& error) {
|
||||||
|
if (!m_db) {
|
||||||
|
error = "Database not open";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
int rc = sqlite3_prepare_v2(m_db, sql.c_str(), static_cast<int>(sql.size()), &stmt, nullptr);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
error = "SQL prepare error: " + std::string(sqlite3_errmsg(m_db));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BindParameters(stmt, params, error)) {
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
|
||||||
|
error = "SQL execution error: " + std::string(sqlite3_errmsg(m_db));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<SqlResult> DatabaseHandle::Query(const std::string& sql, const std::vector<SqlValue>& params,
|
||||||
|
std::string& error) {
|
||||||
|
if (!m_db) {
|
||||||
|
error = "Database not open";
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
int rc = sqlite3_prepare_v2(m_db, sql.c_str(), static_cast<int>(sql.size()), &stmt, nullptr);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
error = "SQL prepare error: " + std::string(sqlite3_errmsg(m_db));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BindParameters(stmt, params, error)) {
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
SqlResult result;
|
||||||
|
int row_count = 0;
|
||||||
|
|
||||||
|
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||||
|
if (row_count >= m_limits.max_result_rows) {
|
||||||
|
error = "Result row limit exceeded";
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int col_count = sqlite3_column_count(stmt);
|
||||||
|
SqlRow row;
|
||||||
|
row.reserve(col_count);
|
||||||
|
|
||||||
|
for (int i = 0; i < col_count; i++) {
|
||||||
|
int type = sqlite3_column_type(stmt, i);
|
||||||
|
switch (type) {
|
||||||
|
case SQLITE_NULL:
|
||||||
|
row.push_back(nullptr);
|
||||||
|
break;
|
||||||
|
case SQLITE_INTEGER:
|
||||||
|
row.push_back(sqlite3_column_int64(stmt, i));
|
||||||
|
break;
|
||||||
|
case SQLITE_FLOAT:
|
||||||
|
row.push_back(sqlite3_column_double(stmt, i));
|
||||||
|
break;
|
||||||
|
case SQLITE_TEXT: {
|
||||||
|
const char* text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, i));
|
||||||
|
int len = sqlite3_column_bytes(stmt, i);
|
||||||
|
row.push_back(std::string(text, len));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SQLITE_BLOB: {
|
||||||
|
const uint8_t* data = static_cast<const uint8_t*>(sqlite3_column_blob(stmt, i));
|
||||||
|
int len = sqlite3_column_bytes(stmt, i);
|
||||||
|
row.push_back(std::vector<uint8_t>(data, data + len));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back(std::move(row));
|
||||||
|
row_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
error = "SQL query error: " + std::string(sqlite3_errmsg(m_db));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t DatabaseHandle::GetLastInsertRowId() const {
|
||||||
|
if (!m_db) return 0;
|
||||||
|
return sqlite3_last_insert_rowid(m_db);
|
||||||
|
}
|
||||||
|
|
||||||
|
int DatabaseHandle::GetChanges() const {
|
||||||
|
if (!m_db) return 0;
|
||||||
|
return sqlite3_changes(m_db);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Lua API
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
struct LuaDatabaseHandle {
|
||||||
|
std::shared_ptr<DatabaseHandle> handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int Lua_DatabaseHandle_Execute(lua_State* L) {
|
||||||
|
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||||
|
if (!lh->handle || !lh->handle->IsOpen()) {
|
||||||
|
lua_pushboolean(L, 0);
|
||||||
|
lua_pushstring(L, "Database not open");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sql = luaL_checkstring(L, 2);
|
||||||
|
|
||||||
|
// Get parameters from optional table
|
||||||
|
std::vector<SqlValue> params;
|
||||||
|
if (lua_gettop(L) >= 3 && lua_istable(L, 3)) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, 3) != 0) {
|
||||||
|
if (lua_isnil(L, -1)) {
|
||||||
|
params.push_back(nullptr);
|
||||||
|
} else if (lua_isinteger(L, -1)) {
|
||||||
|
params.push_back(static_cast<int64_t>(lua_tointeger(L, -1)));
|
||||||
|
} else if (lua_isnumber(L, -1)) {
|
||||||
|
params.push_back(lua_tonumber(L, -1));
|
||||||
|
} else if (lua_isstring(L, -1)) {
|
||||||
|
size_t len;
|
||||||
|
const char* str = lua_tolstring(L, -1, &len);
|
||||||
|
params.push_back(std::string(str, len));
|
||||||
|
} else if (lua_isboolean(L, -1)) {
|
||||||
|
params.push_back(static_cast<int64_t>(lua_toboolean(L, -1)));
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
if (lh->handle->Execute(sql, params, error)) {
|
||||||
|
lua_pushboolean(L, 1);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
lua_pushboolean(L, 0);
|
||||||
|
lua_pushstring(L, error.c_str());
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Lua_DatabaseHandle_Query(lua_State* L) {
|
||||||
|
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||||
|
if (!lh->handle || !lh->handle->IsOpen()) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pushstring(L, "Database not open");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sql = luaL_checkstring(L, 2);
|
||||||
|
|
||||||
|
// Get parameters from optional table
|
||||||
|
std::vector<SqlValue> params;
|
||||||
|
if (lua_gettop(L) >= 3 && lua_istable(L, 3)) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
while (lua_next(L, 3) != 0) {
|
||||||
|
if (lua_isnil(L, -1)) {
|
||||||
|
params.push_back(nullptr);
|
||||||
|
} else if (lua_isinteger(L, -1)) {
|
||||||
|
params.push_back(static_cast<int64_t>(lua_tointeger(L, -1)));
|
||||||
|
} else if (lua_isnumber(L, -1)) {
|
||||||
|
params.push_back(lua_tonumber(L, -1));
|
||||||
|
} else if (lua_isstring(L, -1)) {
|
||||||
|
size_t len;
|
||||||
|
const char* str = lua_tolstring(L, -1, &len);
|
||||||
|
params.push_back(std::string(str, len));
|
||||||
|
} else if (lua_isboolean(L, -1)) {
|
||||||
|
params.push_back(static_cast<int64_t>(lua_toboolean(L, -1)));
|
||||||
|
}
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
auto result = lh->handle->Query(sql, params, error);
|
||||||
|
|
||||||
|
if (!result.has_value()) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pushstring(L, error.c_str());
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create result table
|
||||||
|
lua_createtable(L, static_cast<int>(result->size()), 0);
|
||||||
|
int row_idx = 1;
|
||||||
|
for (const auto& row : *result) {
|
||||||
|
lua_createtable(L, static_cast<int>(row.size()), 0);
|
||||||
|
int col_idx = 1;
|
||||||
|
for (const auto& val : row) {
|
||||||
|
std::visit([L](auto&& arg) {
|
||||||
|
using T = std::decay_t<decltype(arg)>;
|
||||||
|
if constexpr (std::is_same_v<T, std::nullptr_t>) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
} else if constexpr (std::is_same_v<T, int64_t>) {
|
||||||
|
lua_pushinteger(L, arg);
|
||||||
|
} else if constexpr (std::is_same_v<T, double>) {
|
||||||
|
lua_pushnumber(L, arg);
|
||||||
|
} else if constexpr (std::is_same_v<T, std::string>) {
|
||||||
|
lua_pushlstring(L, arg.c_str(), arg.size());
|
||||||
|
} else if constexpr (std::is_same_v<T, std::vector<uint8_t>>) {
|
||||||
|
lua_pushlstring(L, reinterpret_cast<const char*>(arg.data()), arg.size());
|
||||||
|
}
|
||||||
|
}, val);
|
||||||
|
lua_rawseti(L, -2, col_idx++);
|
||||||
|
}
|
||||||
|
lua_rawseti(L, -2, row_idx++);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Lua_DatabaseHandle_LastInsertId(lua_State* L) {
|
||||||
|
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||||
|
if (!lh->handle) {
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, lh->handle->GetLastInsertRowId());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Lua_DatabaseHandle_Changes(lua_State* L) {
|
||||||
|
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||||
|
if (!lh->handle) {
|
||||||
|
lua_pushinteger(L, 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, lh->handle->GetChanges());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Lua_DatabaseHandle_Close(lua_State* L) {
|
||||||
|
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||||
|
if (lh->handle) {
|
||||||
|
lh->handle->Close();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Lua_DatabaseHandle_GC(lua_State* L) {
|
||||||
|
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(luaL_checkudata(L, 1, "DatabaseHandle"));
|
||||||
|
lh->~LuaDatabaseHandle();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const luaL_Reg DatabaseHandle_methods[] = {
|
||||||
|
{"execute", Lua_DatabaseHandle_Execute},
|
||||||
|
{"query", Lua_DatabaseHandle_Query},
|
||||||
|
{"lastInsertId", Lua_DatabaseHandle_LastInsertId},
|
||||||
|
{"changes", Lua_DatabaseHandle_Changes},
|
||||||
|
{"close", Lua_DatabaseHandle_Close},
|
||||||
|
{nullptr, nullptr}
|
||||||
|
};
|
||||||
|
|
||||||
|
static int Lua_Database_Open(lua_State* L) {
|
||||||
|
DatabaseManager* manager = static_cast<DatabaseManager*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||||
|
const char* name = luaL_checkstring(L, 1);
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
auto handle = manager->Open(name, error);
|
||||||
|
|
||||||
|
if (!handle) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
lua_pushstring(L, error.c_str());
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create userdata
|
||||||
|
LuaDatabaseHandle* lh = static_cast<LuaDatabaseHandle*>(lua_newuserdata(L, sizeof(LuaDatabaseHandle)));
|
||||||
|
new (lh) LuaDatabaseHandle{handle};
|
||||||
|
|
||||||
|
luaL_getmetatable(L, "DatabaseHandle");
|
||||||
|
lua_setmetatable(L, -2);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterDatabaseAPI(lua_State* L, DatabaseManager* manager) {
|
||||||
|
// Create DatabaseHandle metatable
|
||||||
|
luaL_newmetatable(L, "DatabaseHandle");
|
||||||
|
|
||||||
|
lua_pushvalue(L, -1);
|
||||||
|
lua_setfield(L, -2, "__index");
|
||||||
|
|
||||||
|
lua_pushcfunction(L, Lua_DatabaseHandle_GC);
|
||||||
|
lua_setfield(L, -2, "__gc");
|
||||||
|
|
||||||
|
luaL_setfuncs(L, DatabaseHandle_methods, 0);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
// Create database table
|
||||||
|
lua_newtable(L);
|
||||||
|
|
||||||
|
// database.open
|
||||||
|
lua_pushlightuserdata(L, manager);
|
||||||
|
lua_pushcclosure(L, Lua_Database_Open, 1);
|
||||||
|
lua_setfield(L, -2, "open");
|
||||||
|
|
||||||
|
// Set as global
|
||||||
|
SetGlobalInRealG(L, "database");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
88
src/main/cpp/sandbox/database_manager.h
Normal file
88
src/main/cpp/sandbox/database_manager.h
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <variant>
|
||||||
|
#include <optional>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
struct sqlite3;
|
||||||
|
struct lua_State;
|
||||||
|
|
||||||
|
namespace mosis {
|
||||||
|
|
||||||
|
// SQL value types
|
||||||
|
using SqlValue = std::variant<std::nullptr_t, int64_t, double, std::string, std::vector<uint8_t>>;
|
||||||
|
using SqlRow = std::vector<SqlValue>;
|
||||||
|
using SqlResult = std::vector<SqlRow>;
|
||||||
|
|
||||||
|
struct DatabaseLimits {
|
||||||
|
size_t max_database_size = 50 * 1024 * 1024; // 50 MB per database
|
||||||
|
int max_databases_per_app = 5; // Max open databases
|
||||||
|
int max_query_time_ms = 5000; // 5 second query timeout
|
||||||
|
int max_result_rows = 10000; // Max rows returned
|
||||||
|
};
|
||||||
|
|
||||||
|
class DatabaseHandle;
|
||||||
|
|
||||||
|
class DatabaseManager {
|
||||||
|
public:
|
||||||
|
DatabaseManager(const std::string& app_id,
|
||||||
|
const std::string& app_root,
|
||||||
|
const DatabaseLimits& limits = DatabaseLimits{});
|
||||||
|
~DatabaseManager();
|
||||||
|
|
||||||
|
// Database operations
|
||||||
|
std::shared_ptr<DatabaseHandle> Open(const std::string& name, std::string& error);
|
||||||
|
void CloseAll();
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
size_t GetOpenDatabaseCount() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_app_id;
|
||||||
|
std::string m_app_root;
|
||||||
|
DatabaseLimits m_limits;
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<DatabaseHandle>> m_databases;
|
||||||
|
|
||||||
|
std::string ResolvePath(const std::string& name);
|
||||||
|
bool ValidateName(const std::string& name, std::string& error);
|
||||||
|
};
|
||||||
|
|
||||||
|
class DatabaseHandle {
|
||||||
|
public:
|
||||||
|
DatabaseHandle(sqlite3* db, const std::string& path, const DatabaseLimits& limits);
|
||||||
|
~DatabaseHandle();
|
||||||
|
|
||||||
|
// Execute (INSERT, UPDATE, DELETE, CREATE, etc.)
|
||||||
|
bool Execute(const std::string& sql, const std::vector<SqlValue>& params, std::string& error);
|
||||||
|
|
||||||
|
// Query (SELECT)
|
||||||
|
std::optional<SqlResult> Query(const std::string& sql, const std::vector<SqlValue>& params,
|
||||||
|
std::string& error);
|
||||||
|
|
||||||
|
// Get last insert rowid
|
||||||
|
int64_t GetLastInsertRowId() const;
|
||||||
|
|
||||||
|
// Get affected rows
|
||||||
|
int GetChanges() const;
|
||||||
|
|
||||||
|
bool IsOpen() const { return m_db != nullptr; }
|
||||||
|
void Close();
|
||||||
|
|
||||||
|
private:
|
||||||
|
sqlite3* m_db;
|
||||||
|
std::string m_path;
|
||||||
|
DatabaseLimits m_limits;
|
||||||
|
|
||||||
|
static int Authorizer(void* user_data, int action, const char* arg1,
|
||||||
|
const char* arg2, const char* arg3, const char* arg4);
|
||||||
|
|
||||||
|
bool BindParameters(void* stmt, const std::vector<SqlValue>& params, std::string& error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register database.* APIs as globals
|
||||||
|
void RegisterDatabaseAPI(lua_State* L, DatabaseManager* manager);
|
||||||
|
|
||||||
|
} // namespace mosis
|
||||||
Reference in New Issue
Block a user