Skip to content

Security

Xyranaut edited this page Jun 1, 2026 · 1 revision

Security

omp-MySQL is built to make the secure choice the default choice. This page explains the model, how to harden a deployment, and what was pentested.

The threat model

A game server is a hostile environment: untrusted players send you input, server files sometimes leak, and traffic crosses networks. The defenses:

Threat Defense in omp-MySQL
Sniffing passwords/data on the wire Mandatory TLS, fail-closed
SQL injection from player input Prepared statements + %e escaping + multi-statement OFF
Stolen password hashes being cracked Argon2id (slow, salted)
Leaked config exposing credentials ${ENV} secrets + least-privilege DB user
Stacked-query injection (; DROP …) multi-statements disabled by default
Runaway/abusive query load opt-in rate / length / pending guards

Transport: TLS is not optional

omp-MySQL refuses to open an unencrypted connection. There is no plaintext fallback. Choose how strictly the certificate is checked with SSL_MODE:

  • SSL_MODE_REQUIRED (0) — encrypt, don't verify the cert (local/dev).
  • SSL_MODE_VERIFY_CA (1) — encrypt + verify the cert chains to a CA you trust.
  • SSL_MODE_VERIFY_IDENTITY (2) — above + the hostname must match (production).

For 1/2, supply the CA with SSL_CA. Also set REQUIRE SSL on the DB user so the server enforces it too. See Configuration for getting the CA.

Credentials: keep them off disk

In order of how much they actually protect you:

  1. ${ENV} secrets (real protection). Put the password in an environment variable, reference it as ${OMP_DB_PASS}. It never sits in the shipped script.
  2. Least-privilege DB user (real protection). Use tools/least-privilege-user.sql to create a user that can only SELECT/INSERT/UPDATE/DELETE its own schema, REQUIRE SSL, host-bound. If the password leaks, the damage is contained — no DROP DATABASE, no reading server files, no other schemas.
  3. Config obfuscation (defense-in-depth only). mysql_config_obfuscate can scramble an .ini. Honest caveat: this is obfuscation, not encryption — and because the source is public, the key is an operator secret you set via OMP_CFG_KEY (never in the source). A dumped server directory yields the blob but not the env-only key. It defeats casual file-grepping, nothing stronger.

Why not "real" client-side encryption? You can't. Any key the component needs to decrypt the config must ship with the component, so anyone with the binary can recover it. Real secrecy comes from keeping the secret off the machine (${ENV}) and limiting what it can do (least privilege).

Injection safety

  • Prepared statements (mysql_prepare + mysql_stmt_set_*) — values travel separately from the SQL text and can never be parsed as SQL. Use them for anything with player input.
  • %e in mysql_format escapes a value via the server's charset-aware escaper.
  • Multi-statements are OFF by default, so a '; DROP TABLE …; -- payload can't stack a second statement.

Note on Row-Level Security (RLS)

MySQL has no built-in RLS (unlike PostgreSQL). It's also the wrong tool here: an open.mp gamemode runs as a single app user that legitimately manages every player's row, and it already scopes every query (WHERE id = ?). RLS guards a trusted-but-limited user from seeing other rows — not this model. For column hiding, use column-level GRANTs.

Pentest & audit — v1.0.0

What was actually tested for v1.0.0 (live, across builds) and passed:

Area Tested Result
SQL injection — login box ' OR 1=1 --, quote payloads in the password field ✅ inert (only hashed + compared)
SQL injection — registration crafted names/payloads through INSERT ✅ inert (bound statement)
SQL injection — command args payloads via /unban, /acmds <x>, … ✅ inert (%e / integer args)
Auth bypass — rejoin log in → quit → rejoin same slot ✅ password required again
Auth bypass — slot reuse imposter on a timed-out player's slot ✅ session wiped; never inherited
Auth persistence ESC/cancel dialog, idle past timeout ✅ frozen until login; cancel/timeout = kick
Re-auth replay re-trigger login/register while logged in ✅ ignored
Privilege bootstrap first-account-becomes-owner ✅ removed — admins only via RCON /setlevel
Memory safety AddressSanitizer + UBSan + static audit ✅ clean (0 errors)
Transport downgrade to plaintext / self-signed cert ✅ fail-closed

Future versions will keep adding to this list, documenting exactly what each release was tested against. If you find a gap, please open an issue — the aim is an honest, growing record.

Hardening checklist

  • SSL_MODE_VERIFY_CA or VERIFY_IDENTITY in production, with a real CA.
  • DB user is least-privilege + REQUIRE SSL + host-bound (not '%').
  • Password in ${ENV}, not in the script/config.
  • All player-input queries use prepared statements.
  • First admin promoted via RCON, not auto-assigned.
  • RCON password is strong and not the default.

Clone this wiki locally