-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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 |
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.
In order of how much they actually protect you:
-
${ENV}secrets (real protection). Put the password in an environment variable, reference it as${OMP_DB_PASS}. It never sits in the shipped script. -
Least-privilege DB user (real protection). Use
tools/least-privilege-user.sqlto create a user that can onlySELECT/INSERT/UPDATE/DELETEits own schema,REQUIRE SSL, host-bound. If the password leaks, the damage is contained — noDROP DATABASE, no reading server files, no other schemas. -
Config obfuscation (defense-in-depth only).
mysql_config_obfuscatecan scramble an.ini. Honest caveat: this is obfuscation, not encryption — and because the source is public, the key is an operator secret you set viaOMP_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).
-
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. -
%einmysql_formatescapes 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.
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.
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.
-
SSL_MODE_VERIFY_CAorVERIFY_IDENTITYin 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.
Understand
Use
- Installing MySQL
- Docker Compose
- Getting started
- Configuration
- SQL crash course
- Designing your tables
- Storing game data
- Dates & times
- First queries
- Async patterns
- Reading results
- Prepared statements
- Passwords & hashing
- Transactions
- Models (active-record)
- Tutorial: login system
- mysql-admin demo
Deeper
Reference