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 = "...")], CheckConstraint SQL 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_ms limit the damage; they are not a defence)
  • TLS to a local host: localhost is not forced onto TLS, and an explicit sslmode / ssl-mode always wins

Requirements

RequirementWhy (CVE class)Proved byDocs
Values are bound parameters, never interpolated (SQLite)Classic SQL injectionrango-tests/tests/integration_sqlite.rs test_sqlite_hostile_values_are_boundSQL injection
Values are bound parameters (PostgreSQL), including JSON keys and values, FTS config and query, array values in PgQueryExtDjango CVE-2024-42005, Sequelize CVE-2026-30951rango-tests/tests/integration.rs test_pg_hostile_values_are_bound
rango-tests/tests/integration.rs test_pg_fts_lang_binds_language
SQL injection
Values are bound parameters (MySQL / MariaDB)Classic SQL injectionrango-tests/tests/integration_mysql.rs test_mysql_hostile_values_are_boundSQL injection
SQLite extension arguments (JSON path and value, FTS5 query) are boundDjango CVE-2024-42005rango-tests/tests/integration_sqlite.rs test_sqlite_hostile_values_are_boundExtensions
Identifiers go through one function, quote_ident, which doubles the quote characterDjango CVE-2021-35042, CVE-2026-1312, CVE-2026-1287; Drizzle CVE-2026-39356rango-orm-core/src/query.rs test_quote_ident_escapes_double_quotes
rango-orm-core/src/query.rs test_quote_ident_mysql_escapes_backticks
rango-orm-core/src/model.rs quote_ident_is_one_token_roundtrip
SQL injection
Identifiers in the query builder (order_by, filters) are quotedsamerango-orm-core/src/filter.rs order_by_quotes_identifier
rango-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 databasesamerango-tests/tests/integration_sqlite.rs test_sqlite_hostile_identifiers_are_quoted
rango-tests/tests/integration.rs test_pg_hostile_identifiers_are_quoted
rango-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)samerango-orm-cli/src/migration/sql/tests.rs hostile_names_are_always_quoted_in_ddl
rango-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-8715rango-orm-cli/tests/migrations_e2e.rs import_does_not_inject_column_names
rango-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 untrustedPostgreSQL CVE-2025-8714, CVE-2025-8715rango-orm-cli/tests/dump_restore_sqlite.rs malicious_names_never_run_injected_sql
rango-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 themsamerango-orm-cli/tests/dump_restore_sqlite.rs hostile_table_and_column_names_stay_identifiersDump and restore
restore is all or nothing (one transaction)Partial restore of a bad filerango-orm-cli/tests/dump_restore_sqlite.rs restore_is_all_or_nothingDump and restore
restore refuses files that are not Rango dumps, and unknown versionsRestoring arbitrary JSONrango-orm-cli/tests/dump_restore_sqlite.rs restore_rejects_foreign_json_and_unknown_versionDump and restore
dump and backup files are written with mode 0600Secrets readable by other local usersrango-orm-cli/tests/dump_restore_sqlite.rs dump_then_restore_round_trips_rows
rango-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 runsIdentifier injection through column namesrango-orm-core/src/model.rs unknown_column_only_accepts_exact_names
rango-tests/tests/integration_sqlite.rs test_unknown_column_is_rejected
rango-tests/tests/integration.rs test_unknown_column_is_rejected
rango-tests/tests/integration_mysql.rs test_unknown_column_is_rejected
SQL injection
Operators and connectors are Rust enums (Op, Connector), never strings from inputDjango CVE-2025-64459, CVE-2025-59681; TypeORM CVE-2025-60542Not 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 itPlaintext passwords at restrango-orm-core/src/fields.rs field_password_hash_is_argon2id
rango-orm-core/src/fields.rs field_password_verify_accepts_correct_and_rejects_wrong
rango-orm-core/tests/field_password_unicode.rs field_password_verify_non_ascii_accents
Field types
FieldPassword prints <redacted> in DebugSecrets in logsrango-orm-core/src/fields.rs field_password_debug_does_not_leak_plaintextField types
FieldPassword has no Serialize or Deserialize implHash or secret leaking through an API responserango-orm-core/src/fields.rs two compile_fail,E0277 doctests on FieldPasswordField types
DatabaseConfig Debug redacts the password in the URLCredentials in logsrango-orm-core/src/config.rs database_config_debug_redacts_url
rango-orm-core/src/config.rs redact_url_replaces_password
rango-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 winTLS downgrade (prefer)rango-orm-postgres/src/lib.rs tls_tests::remote_hosts_require_tls_unless_the_url_says_otherwise
rango-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 winTLS downgrade (PREFERRED)rango-orm-mysql/src/lib.rs tls_tests::remote_hosts_require_tls_unless_the_url_says_otherwiseCredentials
Errors carry no SQL text and no bound valuesData in error messages and logsrango-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 loggedData in logsPartly 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 rootMemory-safety bugs in Rango's own coderango-tests/tests/forbid_unsafe.rs every_crate_root_forbids_unsafe_codeHow Rango is built
cargo audit and cargo deny run on every pushKnown-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.tomlHow Rango is built
rango migrate reads MIGRATE_DATABASE_URL before DATABASE_URL, so the app can run with a DML-only roleOver-privileged application rolerango-orm-cli/tests/sqlite_rebuild.rs migrate_prefers_migrate_database_urlCredentials

Notes

  • Only SQLite asserts the error and span claims directly. PostgreSQL and MySQL map errors through the same db_err shape and open the same db.query span; there is no separate test per backend.
  • Database tests under rango-tests are #[ignore]d unless the matching integration* 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-bench is an unpublished benchmark harness with no crate root, so it carries no forbid(unsafe_code).