Database audit fix

How to Configure secure_file_priv in MySQL Server

MySQL's secure_file_priv decides where the server may read and write files, and an empty value leaves LOAD DATA INFILE and SELECT INTO OUTFILE pointed at the whole filesystem. Set it to NULL or a dedicated directory and restart.

Hiren KalariyaLast reviewed: Sep 25, 2026Check mysql-file-access
Medium
severity
Yes
needs root or sudo
3
results it can return
3 of 6
checks in this audit

Every threshold on this page is transcribed from the database-security-least-privilege audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 6 Least-Privilege Permissions checks, or how the audit runs.

Medium severityNeeds sudo

What this check reads

The check reads two live server variables: SHOW VARIABLES LIKE 'secure_file_priv', keeping the whole row so that a missing row stays distinguishable from an empty value, and SELECT @@local_infile. No row at all means the query failed and the result is SKIP, an empty value means file access is unrestricted and warns, NULL means import and export are disabled entirely, and an explicit path means they are confined to that directory, the last two both passing. local_infile is then applied on top: if it reads 1 or ON the result is forced to WARN regardless of what secure_file_priv holds.

When it applies

Runs only when MySQL/MariaDB is detected and reachable as root over the socket (MYOK=1); otherwise SKIP. It reads two live server variables, so it reflects the running configuration rather than the config file. The script deliberately keeps the whole SHOW VARIABLES row: an empty value means unrestricted (WARN), whereas no row at all means the query failed and produces SKIP rather than a false finding. local_infile being ON forces WARN regardless of how secure_file_priv is set. Note secure_file_priv is read-only at runtime - a change needs a server restart before this check will see it.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPCould not read `secure_file_priv`Nothing was verified. The variable query returned no row, so the file-access setting is unknown rather than safe.
WARN`secure_file_priv` empty, or `local_infile` ONEither server-side file operations are unconfined, or the server can ask a client to hand over a local file, and the message says which.
PASS`NULL` or a directory, with `local_infile` OFFFile import and export are blocked outright or confined to one directory, and no client-side file reads are on offer.

Why it matters

secure_file_priv limits where FILE operations can read/write. NULL disables them; a directory confines them. local_infile enables LOAD DATA LOCAL, where the server asks the client to send a file: a malicious or compromised server (or an injected query on a server reached by a vulnerable client) can read files from the client machine. MySQL 8 defaults local_infile=OFF. CIS MySQL "Ensure 'secure_file_priv' is not empty" and "Ensure 'local_infile' is disabled".

Why it fails, and when it is wrong

  • MySQL 8 packages default secure_file_priv=/var/lib/mysql-files (PASS). MariaDB defaults to empty (WARN) unless set.
  • ETL pipelines that use LOAD DATA LOCAL need local_infile on both sides; prefer LOAD DATA from the secure_file_priv directory, or enable it only for the session/client that needs it.
  • secure_file_priv is read-only at runtime; changing it needs a restart.

How to fix it

[mysqld]
secure_file_priv = NULL        # or a dedicated directory
local_infile = 0

Restart the server.

Verify the fix

sudo mysql -N -e "SHOW VARIABLES LIKE 'secure_file_priv'"
# expected: a value of NULL, or a dedicated directory such as /var/lib/mysql-files

sudo mysql -N -e 'SELECT @@local_infile'
# expected: 0

# Prove export is confined:
sudo mysql -e "SELECT 1 INTO OUTFILE '/tmp/x'"
# expected: ERROR 1290 (--secure-file-priv option ... )

Debugging

MYOK=0; see the Authentication debugging notes.

The SHOW VARIABLES query returned no row at all. This is deliberate: the script refuses to report "unrestricted" when it simply could not read the value. Run the first command above by hand to see the real error (usually a permissions or connection problem, not a configuration one).

It is read-only at runtime, so the change only takes effect after systemctl restart mysql. Confirm the live value with the command above, not the config file.

Then local_infile is ON, which forces WARN on its own. sudo mysql -N -e 'SELECT @@local_infile'; set local_infile=0 in [mysqld] and restart.

Expected. MySQL 8 packages default to /var/lib/mysql-files; MariaDB defaults to empty (unrestricted) unless you set it. This is a real difference, not a check artefact.

LOAD DATA LOCAL needs it on both client and server. Prefer server-side LOAD DATA from the secure_file_priv directory, or enable the option for just the client session that needs it rather than globally.

Sources

How the script reads this

Next

Re-run the Least-Privilege Permissions audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 6 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 6 Least-Privilege Permissions fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals