Security requirements
Every security claim Rango makes, and the test or check that proves it. Rango is written with AI assistance (see How Rango is built); the trust comes from checks you can re-run, not from this page. To report a vulnerability, see SECURITY.md.
Threat model
Trusted (the developer writes it):
- models,
#[field(default = "...")],CheckConstraintSQL and partial-unique conditions - SQL passed to
raw* - migration files
Not trusted:
- end-user input reaching filter values and string column names
- dump files given to
restore/import, and names read from the database catalog - the network between the application and the database
Not protected:
- anything built into trusted SQL from user input (
raw*, constraint SQL, field defaults) rango_orm_core::Filter::raw_where: a hidden, low-level API that takes SQL; never feed it user input- a compromised developer machine, database server, or a dependency flaw not yet in the advisory database
- denial of service through expensive but valid queries (the default 1000-row cap and
statement_timeout_mslimit the damage; they are not a defence) - TLS to a local host: localhost is not forced onto TLS, and an explicit
sslmode/ssl-modealways wins
Requirements
| Requirement | Why (CVE class) | Proved by | Docs |
|---|---|---|---|
| Values are bound parameters, never interpolated (SQLite) | Classic SQL injection | rango-tests/tests/integration_sqlite.rs test_sqlite_hostile_values_are_bound | SQL injection |
Values are bound parameters (PostgreSQL), including JSON keys and values, FTS config and query, array values in PgQueryExt | Django CVE-2024-42005, Sequelize CVE-2026-30951 | rango-tests/tests/integration.rs test_pg_hostile_values_are_boundrango-tests/tests/integration.rs test_pg_fts_lang_binds_language | SQL injection |
| Values are bound parameters (MySQL / MariaDB) | Classic SQL injection | rango-tests/tests/integration_mysql.rs test_mysql_hostile_values_are_bound | SQL injection |
| SQLite extension arguments (JSON path and value, FTS5 query) are bound | Django CVE-2024-42005 | rango-tests/tests/integration_sqlite.rs test_sqlite_hostile_values_are_bound | Extensions |
Identifiers go through one function, quote_ident, which doubles the quote character | Django CVE-2021-35042, CVE-2026-1312, CVE-2026-1287; Drizzle CVE-2026-39356 | rango-orm-core/src/query.rs test_quote_ident_escapes_double_quotesrango-orm-core/src/query.rs test_quote_ident_mysql_escapes_backticksrango-orm-core/src/model.rs quote_ident_is_one_token_roundtrip | SQL injection |
Identifiers in the query builder (order_by, filters) are quoted | same | rango-orm-core/src/filter.rs order_by_quotes_identifierrango-orm-postgres/src/query_builder/mod.rs order_by_quotes_identifier | SQL injection |
| Identifiers in the CRUD ops (insert, update, delete, filter on hostile table and column names) are quoted, run against a real database | same | rango-tests/tests/integration_sqlite.rs test_sqlite_hostile_identifiers_are_quotedrango-tests/tests/integration.rs test_pg_hostile_identifiers_are_quotedrango-tests/tests/integration_mysql.rs test_mysql_hostile_identifiers_are_quoted | SQL injection |
| Identifiers in the CLI's generated DDL are quoted (all three backends) | same | rango-orm-cli/src/migration/sql/tests.rs hostile_names_are_always_quoted_in_ddlrango-orm-cli/tests/dump_restore_sqlite.rs hostile_table_and_column_names_stay_identifiers | SQL injection |
import treats column names in the file as untrusted (PostgreSQL, MySQL) | PostgreSQL CVE-2025-8714, CVE-2025-8715 | rango-orm-cli/tests/migrations_e2e.rs import_does_not_inject_column_namesrango-orm-cli/tests/migrations_e2e_mysql.rs import_does_not_inject_column_names | Import |
restore treats table and column names, and schema SQL fragments, in a dump as untrusted | PostgreSQL CVE-2025-8714, CVE-2025-8715 | rango-orm-cli/tests/dump_restore_sqlite.rs malicious_names_never_run_injected_sqlrango-orm-cli/src/migration/snapshot.rs to_schemas_round_trips_and_rejects_injected_sql | Dump and restore |
dump / export read table and column names from the database catalog and quote them | same | rango-orm-cli/tests/dump_restore_sqlite.rs hostile_table_and_column_names_stay_identifiers | Dump and restore |
restore is all or nothing (one transaction) | Partial restore of a bad file | rango-orm-cli/tests/dump_restore_sqlite.rs restore_is_all_or_nothing | Dump and restore |
restore refuses files that are not Rango dumps, and unknown versions | Restoring arbitrary JSON | rango-orm-cli/tests/dump_restore_sqlite.rs restore_rejects_foreign_json_and_unknown_version | Dump and restore |
dump and backup files are written with mode 0600 | Secrets readable by other local users | rango-orm-cli/tests/dump_restore_sqlite.rs dump_then_restore_round_trips_rowsrango-orm-cli/tests/dump_restore_sqlite.rs backup_is_gzip_private_and_restorable | Dump and restore |
A string column name (eq, order_by, subqueries, values, aggregates, extensions) must exist in the model schema; otherwise Error::Invalid before any SQL runs | Identifier injection through column names | rango-orm-core/src/model.rs unknown_column_only_accepts_exact_namesrango-tests/tests/integration_sqlite.rs test_unknown_column_is_rejectedrango-tests/tests/integration.rs test_unknown_column_is_rejectedrango-tests/tests/integration_mysql.rs test_unknown_column_is_rejected | SQL injection |
Operators and connectors are Rust enums (Op, Connector), never strings from input | Django CVE-2025-64459, CVE-2025-59681; TypeORM CVE-2025-60542 | Not covered by a test: it holds by construction. Op and Connector are private enums in rango-orm-core/src/filter.rs, and no public method takes an operator or connector as a string. Checked by review. | SQL injection |
FieldPassword stores an Argon2id hash and verify checks a candidate against it | Plaintext passwords at rest | rango-orm-core/src/fields.rs field_password_hash_is_argon2idrango-orm-core/src/fields.rs field_password_verify_accepts_correct_and_rejects_wrongrango-orm-core/tests/field_password_unicode.rs field_password_verify_non_ascii_accents | Field types |
FieldPassword prints <redacted> in Debug | Secrets in logs | rango-orm-core/src/fields.rs field_password_debug_does_not_leak_plaintext | Field types |
FieldPassword has no Serialize or Deserialize impl | Hash or secret leaking through an API response | rango-orm-core/src/fields.rs two compile_fail,E0277 doctests on FieldPassword | Field types |
DatabaseConfig Debug redacts the password in the URL | Credentials in logs | rango-orm-core/src/config.rs database_config_debug_redacts_urlrango-orm-core/src/config.rs redact_url_replaces_passwordrango-orm-core/src/config.rs redact_url_handles_special_chars_and_query | Credentials |
Remote PostgreSQL hosts default to sslmode=require; localhost and an explicit sslmode / PGSSLMODE win | TLS downgrade (prefer) | rango-orm-postgres/src/lib.rs tls_tests::remote_hosts_require_tls_unless_the_url_says_otherwiserango-orm-core/src/config.rs local_hosts_are_not_forced_to_tls | Credentials |
Remote MySQL / MariaDB hosts default to ssl-mode=REQUIRED; localhost and an explicit mode win | TLS downgrade (PREFERRED) | rango-orm-mysql/src/lib.rs tls_tests::remote_hosts_require_tls_unless_the_url_says_otherwise | Credentials |
| Errors carry no SQL text and no bound values | Data in error messages and logs | rango-tests/tests/integration_sqlite.rs test_sqlite_bound_values_never_logged_or_in_errors (SQLite; the other backends share the same db_err shape, see the note below) | Error handling |
| Bound values are never logged | Data in logs | Partly covered. The same test checks Rango's own db.query spans. sqlx's own statement log is not asserted: its QueryLogger struct holds only the SQL text and counters (no arguments), see sqlx-core/src/logger.rs in sqlx 0.9. Re-check on every sqlx bump. | Observability |
#![forbid(unsafe_code)] in every crate root | Memory-safety bugs in Rango's own code | rango-tests/tests/forbid_unsafe.rs every_crate_root_forbids_unsafe_code | How Rango is built |
cargo audit and cargo deny run on every push | Known-vulnerable or unlicensed dependencies (RUSTSEC-2024-0363, RUSTSEC-2024-0365 need sqlx 0.8.1 or later) | Not a test: CI job audit in .github/workflows/ci.yml, policy in deny.toml | How Rango is built |
rango migrate reads MIGRATE_DATABASE_URL before DATABASE_URL, so the app can run with a DML-only role | Over-privileged application role | rango-orm-cli/tests/sqlite_rebuild.rs migrate_prefers_migrate_database_url | Credentials |
Notes
- Only SQLite asserts the error and span claims directly. PostgreSQL and MySQL map errors through the same
db_errshape and open the samedb.queryspan; there is no separate test per backend. - Database tests under
rango-testsare#[ignore]d unless the matchingintegration*feature is on (just test-sqlite,just test-postgres,just test-mysql). - Claims marked "Not covered by a test" hold by construction or by CI policy; they are listed so nobody mistakes them for tested.
rango-benchis an unpublished benchmark harness with no crate root, so it carries noforbid(unsafe_code).