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 | When | What it means |
|---|---|---|
| SKIP | Could 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` ON | Either 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` OFF | File 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 LOCALneedlocal_infileon both sides; preferLOAD DATAfrom thesecure_file_privdirectory, or enable it only for the session/client that needs it. secure_file_privis read-only at runtime; changing it needs a restart.
How to fix it
[mysqld]
secure_file_priv = NULL # or a dedicated directory
local_infile = 0Restart 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
- MySQL 8.4: secure_file_priv
- MySQL 8.4: Security Considerations for LOAD DATA LOCAL
- MySQL 8.4: local_infile
- MariaDB KB: secure_file_priv
- MariaDB KB: LOAD DATA INFILE / LOCAL security
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