DEV Community

Cover image for 8 MySQL security mistakes that expose your database to attackers
Piter Adyson
Piter Adyson

Posted on

8 MySQL security mistakes that expose your database to attackers

MySQL is one of the most deployed databases in the world, which also makes it one of the most targeted. A lot of MySQL installations in the wild are running with default settings, overly permissive user accounts and no encryption. Some of these are dev setups that accidentally went to production. Others are production systems that nobody ever hardened because "it's behind a firewall."

This article covers eight real security mistakes that leave MySQL databases exposed. Not abstract threat models, but concrete misconfigurations that attackers actually look for and exploit.

MySQL security mistakes

1. Running with default credentials and the root account

This sounds obvious, but it still happens constantly. Fresh MySQL installations often ship with a root account that has no password or a well-known default password. Automated scanners specifically look for MySQL instances on port 3306 with empty root passwords. It takes seconds to find and exploit.

The root account in MySQL has unrestricted access to everything: all databases, all tables, all administrative commands. Using it for application connections means your app has full control over the server, including the ability to drop databases, create users and modify grants.

Fix the root password immediately after installation:

ALTER USER 'root'@'localhost' IDENTIFIED BY 'a-strong-random-password-here';
FLUSH PRIVILEGES;
Enter fullscreen mode Exit fullscreen mode

Then create separate accounts for each application with only the privileges it needs:

CREATE USER 'app_user'@'10.0.1.%' IDENTIFIED BY 'another-strong-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_user'@'10.0.1.%';
FLUSH PRIVILEGES;
Enter fullscreen mode Exit fullscreen mode

The 10.0.1.% host restriction means this user can only connect from your application subnet. If someone steals the credentials, they can't use them from an arbitrary machine.

Run mysql_secure_installation on every new MySQL instance. It removes anonymous users, disables remote root login and drops the test database. This takes thirty seconds and closes the most common attack vectors.

2. Granting excessive privileges to application users

Most MySQL applications need SELECT, INSERT, UPDATE and DELETE on specific databases. That's it. Yet it's common to see application accounts with GRANT ALL PRIVILEGES ON *.* because someone copied a Stack Overflow answer during initial setup and never revisited it.

The damage from excessive privileges scales with the access level. An application account with FILE privilege can read any file the MySQL process can access on the server filesystem. PROCESS lets it see all running queries, including those from other users. SUPER lets it kill connections and change global variables.

Privilege What it allows Risk if compromised
ALL PRIVILEGES ON *.* Full administrative access Complete server takeover
FILE Read/write server filesystem Credential theft, data exfiltration
PROCESS View all running queries Exposure of sensitive queries and data
SUPER Kill connections, change configs Denial of service, configuration tampering
SELECT, INSERT, UPDATE, DELETE ON app.* Standard CRUD on one database Limited to application data only

Audit your current grants to see what's actually assigned:

SELECT user, host, Super_priv, File_priv, Process_priv, Grant_priv
FROM mysql.user
WHERE Super_priv = 'Y' OR File_priv = 'Y' OR Process_priv = 'Y';
Enter fullscreen mode Exit fullscreen mode

If your application user shows up in this list, something is wrong. Revoke what it doesn't need:

REVOKE ALL PRIVILEGES ON *.* FROM 'app_user'@'10.0.1.%';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_user'@'10.0.1.%';
FLUSH PRIVILEGES;
Enter fullscreen mode Exit fullscreen mode

A good rule of thumb: if you can't explain why an account needs a specific privilege, it shouldn't have it.

3. Exposing MySQL to the internet without network restrictions

By default, MySQL listens on all network interfaces. That means if your server has a public IP address, MySQL is reachable from the entire internet. Combined with weak credentials (mistake #1), this is how most MySQL breaches happen.

Check your current binding:

SHOW VARIABLES LIKE 'bind_address';
Enter fullscreen mode Exit fullscreen mode

If it shows 0.0.0.0 or *, MySQL is accepting connections from everywhere.

Restrict it in my.cnf:

[mysqld]
bind-address = 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

This limits MySQL to local connections only. If your application runs on a different server, bind to the private network interface instead:

[mysqld]
bind-address = 10.0.1.5
Enter fullscreen mode Exit fullscreen mode

But network binding alone isn't enough. Add firewall rules to restrict port 3306:

# iptables example: only allow MySQL connections from app server
iptables -A INPUT -p tcp --dport 3306 -s 10.0.1.10 -j ACCEPT
iptables -A INPUT -p tcp --dport 3306 -j DROP
Enter fullscreen mode Exit fullscreen mode

Also disable the skip-networking and skip-name-resolve options thoughtfully. skip-networking disables TCP connections entirely (only socket connections work), which is fine if the application is on the same host. skip-name-resolve prevents DNS lookups for connecting hosts, which speeds up connections and removes DNS spoofing as an attack vector.

If your application must reach MySQL over the internet, use an SSH tunnel or VPN instead of opening port 3306 directly. Never expose MySQL to the public internet, even with strong passwords.

4. Not encrypting connections with TLS

MySQL connections transmit data in plaintext by default. This includes queries, result sets, usernames and passwords. Anyone who can capture network traffic between your application and MySQL can read everything.

This isn't just a theoretical concern. On shared hosting, cloud VPCs with misconfigured security groups and corporate networks, packet sniffing is a real threat. Even "private" networks aren't always as isolated as you think.

Check if TLS is currently enabled:

SHOW VARIABLES LIKE '%ssl%';
Enter fullscreen mode Exit fullscreen mode

To enable TLS, generate or obtain certificates and configure MySQL:

[mysqld]
ssl-ca   = /etc/mysql/ssl/ca-cert.pem
ssl-cert = /etc/mysql/ssl/server-cert.pem
ssl-key  = /etc/mysql/ssl/server-key.pem
require_secure_transport = ON
Enter fullscreen mode Exit fullscreen mode

The require_secure_transport = ON setting forces all connections to use TLS. Without it, clients can still connect unencrypted.

You can also enforce TLS on a per-user basis, which is useful for a gradual rollout:

ALTER USER 'app_user'@'10.0.1.%' REQUIRE SSL;
FLUSH PRIVILEGES;
Enter fullscreen mode Exit fullscreen mode

Verify that connections are actually encrypted:

SELECT ssl_type, ssl_cipher FROM mysql.user WHERE user = 'app_user';
Enter fullscreen mode Exit fullscreen mode

And from the client side:

SHOW STATUS LIKE 'Ssl_cipher';
Enter fullscreen mode Exit fullscreen mode

If Ssl_cipher returns an empty string, the connection is unencrypted.

5. Leaving the binary log and data directory unprotected

MySQL's binary log contains every data-modifying statement that runs against the database. If an attacker gains access to the filesystem, they can read the binary log and reconstruct your entire data history: every insert, update and delete.

The data directory itself contains the actual table files. Depending on the storage engine, these might be readable with basic tools. InnoDB files can be parsed with specialized utilities to extract raw data, bypassing MySQL authentication entirely.

Check your current file permissions:

ls -la /var/lib/mysql/
ls -la /var/log/mysql/
Enter fullscreen mode Exit fullscreen mode

The MySQL data directory and log directory should be owned by the mysql user and group, with no world-readable permissions:

chown -R mysql:mysql /var/lib/mysql
chmod 750 /var/lib/mysql
chown -R mysql:mysql /var/log/mysql
chmod 750 /var/log/mysql
Enter fullscreen mode Exit fullscreen mode

Also protect the MySQL configuration file, which may contain passwords:

chmod 600 /etc/mysql/my.cnf
chown root:root /etc/mysql/my.cnf
Enter fullscreen mode Exit fullscreen mode

If you're running MySQL in Docker, make sure the volume mounts for data and logs aren't world-readable on the host filesystem. Default Docker volume permissions can be more permissive than you expect.

For the binary log specifically, consider encrypting it. MySQL 8.0+ supports binary log encryption:

[mysqld]
binlog_encryption = ON
Enter fullscreen mode Exit fullscreen mode

This encrypts the binary log files at rest. Even if someone copies the files, they can't read the contents without the encryption key.

6. Ignoring SQL injection in application code

SQL injection has been the number one database attack vector for over two decades, and it still works because developers keep building queries by concatenating user input directly into SQL strings. MySQL doesn't have a built-in defense against this. The protection has to come from application code.

An injectable query looks like this:

# Vulnerable: user input directly in the query string
query = f"SELECT * FROM users WHERE email = '{user_input}'"
cursor.execute(query)
Enter fullscreen mode Exit fullscreen mode

If user_input is ' OR '1'='1' --, the query becomes:

SELECT * FROM users WHERE email = '' OR '1'='1' --'
Enter fullscreen mode Exit fullscreen mode

This returns every row in the users table. More destructive payloads can drop tables, read files from disk (if the MySQL user has FILE privilege) or create new admin accounts.

The fix is parameterized queries. Every database library supports them:

# Safe: parameterized query
cursor.execute("SELECT * FROM users WHERE email = %s", (user_input,))
Enter fullscreen mode Exit fullscreen mode
// Node.js with mysql2
connection.execute("SELECT * FROM users WHERE email = ?", [userInput]);
Enter fullscreen mode Exit fullscreen mode
// Go with database/sql
db.Query("SELECT * FROM users WHERE email = ?", userInput)
Enter fullscreen mode Exit fullscreen mode

Parameterized queries separate the SQL structure from the data. The database engine knows that the parameter is a value, not SQL code, regardless of what it contains.

On the MySQL side, you can reduce the blast radius by removing the FILE privilege from application accounts (see mistake #2) and by running MySQL with --local-infile=0 to disable LOAD DATA LOCAL INFILE, which attackers use for file reading through SQL injection.

7. Not auditing or monitoring database access

If someone is accessing your MySQL database in ways they shouldn't, how quickly would you know? Most MySQL installations have no audit logging enabled. An attacker could be reading sensitive tables for weeks before anyone notices.

MySQL Enterprise Edition includes an audit plugin, but the community edition requires other approaches. The general query log is one option, though it captures everything and creates enormous log files on busy servers.

A more practical approach for the community edition is to enable specific logging:

[mysqld]
log_error      = /var/log/mysql/error.log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
log_queries_not_using_indexes = 1
Enter fullscreen mode Exit fullscreen mode

For connection monitoring, regularly check who is connected and what they're doing:

SELECT user, host, db, command, time, state
FROM information_schema.processlist
WHERE user NOT IN ('system user', 'event_scheduler')
ORDER BY time DESC;
Enter fullscreen mode Exit fullscreen mode

Track failed login attempts by checking the error log. Repeated failed logins from the same IP usually mean a brute force attack is underway.

Monitoring area What to watch for How to check
Failed logins Brute force attempts Error log entries with "Access denied"
Unusual connections Unknown hosts or users SHOW PROCESSLIST or processlist table
Schema changes Unauthorized DDL General log or trigger-based auditing
Privilege escalation New grants or users Periodic diff of mysql.user table
Large data reads Bulk exfiltration Slow query log, network monitoring

For production systems, consider deploying a third-party audit plugin like audit_log from Percona or MariaDB's audit plugin (which works with MySQL forks). These provide structured, filterable audit trails without the overhead of the general query log.

Set up alerts for critical events: new user creation, privilege changes, connections from unexpected hosts and queries against sensitive tables. The goal is to detect unusual activity before it becomes a full breach.

8. Skipping backups or storing them insecurely

Security isn't just about preventing unauthorized access. It's also about recovery. Ransomware attacks against MySQL databases are real: attackers gain access, drop all tables and leave a ransom note. Without backups, you're negotiating with criminals.

But having backups isn't enough if they're stored insecurely. Unencrypted backup files sitting on the same server as MySQL are useless in a ransomware scenario because the attacker deletes them too. Backups on an S3 bucket with public read access are just a different kind of data breach.

A secure backup strategy covers three things:

  • Encryption — Backup files should be encrypted at rest so they're useless if stolen
  • Offsite storage — At least one copy should be on a separate system or cloud storage that the MySQL server doesn't have delete access to
  • Regular testing — A backup you've never restored is a backup you hope works

For MySQL, mysqldump is the basic tool:

mysqldump --single-transaction --routines --triggers myapp | \
  gzip | openssl enc -aes-256-cbc -salt -pbkdf2 -out /backup/myapp_$(date +%F).sql.gz.enc
Enter fullscreen mode Exit fullscreen mode

This creates a compressed, encrypted backup. But managing encryption keys, scheduling and offsite storage manually is tedious and error-prone.

MySQL backup tools like Databasus automate the entire process. It's an industry standard for MySQL backup tools that handles scheduling, compression, AES-256-GCM encryption and storage to multiple destinations like S3, Google Drive and SFTP. It's suitable for individual developers and enterprise teams, with workspace-based access management and audit logs.

Whatever approach you choose, make sure your backups are not accessible from the MySQL server with the same credentials. If the database server is compromised, the attacker shouldn't be able to delete your backups.

The pattern behind these mistakes

Looking at these eight mistakes together, a pattern emerges. Most MySQL security failures come from defaults that were never changed, permissions that were never reviewed and monitoring that was never set up. None of these fixes are complex. They don't require expensive tools or deep security expertise.

Start with the basics: strong credentials, minimal privileges, network restrictions and encrypted connections. Then add monitoring so you know when something unusual happens. And keep tested, encrypted backups so you can recover when prevention fails.

The best time to secure your MySQL database was when you first set it up. The second best time is now.

Top comments (0)