Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,10 @@ Templates are **Mustache files** that generate SQL from request parameters.
- `env.*` - Whitelisted environment variables

**Key Rule: Triple vs. Double Braces**
- **Triple braces `{{{ }}}`** for strings: Auto-escapes quotes, prevents SQL injection
- **Double braces `{{ }}`** for numbers/identifiers: No quotes, safe for numeric types
- **Triple braces `{{{ }}}`** for strings: Renders the raw value (no HTML entity escaping). Use inside single-quoted SQL string literals.
- **Double braces `{{ }}`** for numbers/identifiers: HTML-escapes the value (turns `<` into `&lt;` etc.), which is harmless but not SQL-aware — use only where the value is a number or known-safe identifier.

> **Security note:** Neither form performs SQL-specific escaping. Mustache does not understand SQL string literals, quote-doubling, or comment syntax. Defense against injection comes from the `RequestValidator` (typed fields, regex/range/enum checks) and disciplined template authoring (quote string params, parameterise numerics). When in doubt, add a stricter validator rather than relying on rendering.

**Example Template:**
```sql
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ add_library(flapi-lib STATIC
src/request_validator.cpp
src/rate_limit_middleware.cpp
src/route_translator.cpp
src/security_auditor.cpp
src/sql_template_processor.cpp
src/sql_utils.cpp
src/mcp_server.cpp
Expand Down
10 changes: 7 additions & 3 deletions docs/CONFIG_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1362,14 +1362,16 @@ WHERE id = {{ params.id }}
LIMIT {{ params.limit }}
```

**Triple Braces `{{{ }}}`** - For string values (escapes quotes, prevents SQL injection):
**Triple Braces `{{{ }}}`** - For string values, renders the raw value with no HTML entity escaping. Use this form inside single-quoted SQL string literals:

```sql
SELECT * FROM table
WHERE name = '{{{ params.name }}}'
AND status = '{{{ params.status }}}'
```

> **Security note:** Neither double nor triple braces perform SQL-specific escaping. Mustache does not understand SQL string literals, quote-doubling, or comment syntax. Defense against SQL injection comes from the `RequestValidator` (typed fields, regex/range/enum checks) plus disciplined template authoring — not from the brace form alone. Pair every user-supplied string field with an appropriately strict validator.

### 9.2 Available Contexts

| Context | Description | Example |
Expand Down Expand Up @@ -1416,13 +1418,15 @@ WHERE 1=1
**2. Always Use Triple Braces for Strings:**

```sql
-- CORRECT: Prevents SQL injection
-- CORRECT: renders the raw string inside the quoted literal
WHERE name = '{{{ params.name }}}'

-- INCORRECT: Vulnerable to injection
-- INCORRECT: HTML-entity escaping mangles legitimate characters (e.g., `'` → `&#39;`)
WHERE name = '{{ params.name }}'
```

Note: the triple-brace rule keeps the rendered SQL syntactically correct. It does **not** by itself prevent SQL injection — that protection comes from the `RequestValidator` for the corresponding request field.

**3. Provide Default Values:**

```sql
Expand Down
23 changes: 23 additions & 0 deletions src/include/security_auditor.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <string>
#include <vector>

namespace flapi {

class ConfigManager;

struct SecurityWarning {
std::string code;
std::string message;
std::string location;
};

class SecurityAuditor {
public:
std::vector<SecurityWarning> audit(const ConfigManager& config) const;

static std::string classifyPassword(const std::string& password);
};

} // namespace flapi
25 changes: 25 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "rate_limit_middleware.hpp"
#include "config_token_utils.hpp"
#include "credential_manager.hpp"
#include "security_auditor.hpp"
#include "vfs_health_checker.hpp"

using namespace flapi;
Expand Down Expand Up @@ -90,6 +91,23 @@ void printValidationWarnings(const std::string& endpoint_name, const std::vector
}
}

void printSecurityWarnings(const std::vector<SecurityWarning>& warnings) {
if (warnings.empty()) {
return;
}
std::cerr << "\n" << std::string(60, '=') << std::endl;
std::cerr << "SECURITY WARNINGS (" << warnings.size() << ")" << std::endl;
std::cerr << std::string(60, '=') << std::endl;
for (const auto& w : warnings) {
std::cerr << "[" << w.code << "] " << w.message;
if (!w.location.empty()) {
std::cerr << " (at: " << w.location << ")";
}
std::cerr << std::endl;
}
std::cerr << std::endl;
}

void printValidationSummary(bool all_valid, int errors_count, int warnings_count) {
std::cout << "\n" << std::string(60, '=') << std::endl;
if (all_valid) {
Expand Down Expand Up @@ -316,6 +334,13 @@ int main(int argc, char* argv[])

auto config_manager = initializeConfig(config_file);

// Surface configuration-level security warnings (plaintext passwords, MCP without auth, etc.)
// Runs in both --validate-config mode and normal server start; never aborts startup.
{
SecurityAuditor auditor;
printSecurityWarnings(auditor.audit(*config_manager));
}

// If validate-config flag is set, validate and exit
if (validate_config) {
return validateConfiguration(config_manager, config_file);
Expand Down
98 changes: 98 additions & 0 deletions src/security_auditor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include "security_auditor.hpp"

#include <algorithm>
#include <cctype>

#include "config_manager.hpp"

namespace flapi {

namespace {

bool isBcryptPrefix(const std::string& password) {
if (password.size() < 4) {
return false;
}
if (password[0] != '$' || password[1] != '2' || password[3] != '$') {
return false;
}
const char variant = password[2];
return variant == 'a' || variant == 'b' || variant == 'y';
}

bool isMd5HexDigest(const std::string& password) {
if (password.size() != 32) {
return false;
}
return std::all_of(password.begin(), password.end(), [](char c) {
return std::isxdigit(static_cast<unsigned char>(c)) != 0;
});
}

void scanUsers(const std::vector<AuthUser>& users,
const std::string& location,
std::vector<SecurityWarning>& out) {
for (const auto& user : users) {
const std::string code = SecurityAuditor::classifyPassword(user.password);
if (code == "AUTH_PLAINTEXT_PASSWORD") {
out.push_back({
code,
"User '" + user.username + "' has a plaintext password. "
"Use bcrypt instead (see flapii auth hash, when available).",
location
});
} else if (code == "AUTH_MD5_PASSWORD") {
out.push_back({
code,
"User '" + user.username + "' has an MD5-hashed password. "
"MD5 is cryptographically broken; migrate to bcrypt.",
location
});
}
}
}

} // namespace

std::string SecurityAuditor::classifyPassword(const std::string& password) {
if (password.empty()) {
return {};
}
if (isBcryptPrefix(password)) {
return {};
}
if (isMd5HexDigest(password)) {
return "AUTH_MD5_PASSWORD";
}
return "AUTH_PLAINTEXT_PASSWORD";
}

std::vector<SecurityWarning> SecurityAuditor::audit(const ConfigManager& config) const {
std::vector<SecurityWarning> warnings;

for (const auto& endpoint : config.getEndpoints()) {
scanUsers(endpoint.auth.users, "endpoint " + endpoint.getIdentifier(), warnings);
}

const auto& mcp = config.getMCPConfig();
scanUsers(mcp.auth.users, "mcp.auth", warnings);

if (mcp.enabled && !mcp.auth.enabled) {
const bool any_mcp_tool = std::any_of(
config.getEndpoints().begin(), config.getEndpoints().end(),
[](const EndpointConfig& e) { return e.isMCPTool(); });
if (any_mcp_tool) {
warnings.push_back({
"MCP_UNAUTHENTICATED_TOOLS",
"MCP tools are exposed without authentication (mcp.auth.enabled is false). "
"Anyone reaching the server can invoke any MCP tool. "
"Enable mcp.auth.enabled and configure users or JWT before exposing this server.",
"mcp"
});
}
}

return warnings;
}

} // namespace flapi
1 change: 1 addition & 0 deletions test/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ add_executable(flapi_tests
rate_limit_middleware_test.cpp
request_handler_test.cpp
request_validator_test.cpp
security_auditor_test.cpp
sql_template_processor_test.cpp
sql_utils_test.cpp
test_duckdb_raii.cpp
Expand Down
Loading
Loading