Security roadmap shipped — MCP RBAC, prepared statements, PBKDF2, audit log
flAPI v26.05.17 is out. This release answers a recurring question from operators evaluating flAPI for AI-agent access to real data warehouses: "Is this safe to expose to MCP-driven agents against production?"
The answer is now: yes, with single-line opt-ins — flapii project init demos still work without auth, and every production control is one YAML key away.
What's new
MCP hardening (the headline)
| Feature | YAML | Purpose |
|---|---|---|
| Per-tool RBAC | mcp-tool.allowed-roles: [analyst] | Restrict each tool to JWT/OIDC principals carrying one of the named roles. Deny-by-default when mcp.auth.enabled: true — a tool without allowed-roles refuses every call. |
| Shadow / dry-run | _dryRun: true in tools/call args | Run validators + template expansion + EXPLAIN and return the rendered SQL + plan, without executing. Lets operators audit every tool definition before production. |
| Tool-description hygiene | mcp.strict-descriptions: true | Refuses to start if any mcp-tool.description contains control characters, JSON-breakout patterns, or role-override phrases ("ignore previous instructions"). |
| Response shaping | mcp-tool.response: { max-rows, redact-columns, sample } | Hard row caps, per-column redaction, or summary-only sampling — applied before the result reaches the agent. |
| Per-tool rate limit | mcp-tool.rate-limit: { enabled, max, interval } | Per-principal budget independent of the global rate limiter. |
SQL-injection defense
flAPI's SQL templates use Mustache. Typed double-brace references are now rewritten to DuckDB prepared-statement placeholders:
-- Field has `validators: [{type: int}]`. The renderer emits `?`
-- and binds via duckdb_bind_int64. SQL injection at this site is
-- structurally impossible.
WHERE id = {{ params.id }}
-- Triple-brace still flows through Mustache as text — use for LIKE
-- patterns and other text-mode use sites.
WHERE name LIKE '%{{{ params.name }}}%'
This applies to int, double, boolean, date, time, uuid, enum, email, and string types. The end-to-end injection corpus at test/integration/test_sql_injection_corpus.py exercises 37 classic payloads — every one returns zero rows.
General production wins
- PBKDF2-SHA256 password hashing —
auth.users[*].passwordnow accepts$pbkdf2-sha256$<iter>$<b64-salt>$<b64-hash>MCF strings (OpenSSL, 600 k iterations). Compatible with Pythonpasslib. Plaintext and MD5 still work but the startup auditor warns about them. - Config-driven CORS allowlist —
cors.allow-origins: [...]. The historic wildcard is gone; the auditor warns if it's combined withauth.enabled: true. - JSONL request audit log —
audit: { enabled, sink, path, redact: [...] }emits one JSON line per REST + MCP request with principal, target, params, status, row count, latency. - Per-user rate limit —
rate-limit.key: ip | user | user-or-ip.user-or-ipis the recommended setting for share-NAT scenarios. - TLS in the embedded server —
https: { enabled, ssl_cert_file, ssl_key_file }wires Crow's OpenSSL chain. Reverse-proxy termination is still recommended; direct TLS is supported for self-contained deployments. - Startup security auditor — at boot, flAPI scans the loaded config and emits structured warnings for plaintext passwords, MD5 passwords, MCP exposed without auth on a non-loopback bind, and CORS wildcard + auth combinations.
Honest docs
The misleading claim that {{{ }}} "prevents SQL injection" is gone from the docs. The actual layered defense (validators → prepared bind → regex fallback for triple-brace and untyped fields) is documented in CONFIG_REFERENCE.md.
Migration notes
Existing configs keep working unchanged:
- MCP without auth stays simple — no
allowed-rolesrequired. - REST endpoints with typed validators automatically benefit from the prepared-statement defense; no template changes needed.
- MD5 and plaintext passwords still verify; you'll see a deprecation warning at boot.
- CORS wildcard is still accepted, with a warning when paired with auth.
To opt into the production hardening, see docs/getting-started/configuration.md for end-to-end YAML examples.
License
This release coincides with the relicense from Apache 2.0 to the Business Source License (BSL) v1.1. The BSL is source-available; non-production use is permitted without a commercial agreement. The Change License (MPL 2.0) takes effect five years after first publication of each version. Full text in LICENSE.
Full release notes: CHANGELOG.md