Database audit fix

How to Disable & Revoke MySQL FILE Privilege on Linux

The MySQL FILE privilege lets any session read and write files as the mysqld OS user, turning a single SQL injection into arbitrary file read and a webshell write. No application account should hold it: revoke FILE ON *.* from each.

Hiren KalariyaLast reviewed: Sep 25, 2026Check mysql-file-privilege
Medium
severity
Yes
needs root or sudo
3
results it can return
2 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 the global File_priv column of mysql.user with SELECT user,host FROM mysql.user WHERE File_priv='Y', excluding a fixed system list: root, mysql.session, mysql.sys, mysql.infoschema, mariadb.sys, mysql, debian-sys-maint and percona.telemetry. Anything still returned is an account that can read and write files on the server through SQL. The threshold is a single row: one non-system holder is a FAIL that names every user@host pair it found, and only an empty result passes.

When it applies

Runs only when MySQL/MariaDB is detected and reachable as root over the socket (MYOK=1); otherwise SKIP. It reads the global File_priv column of mysql.user, so it covers global grants only. A fixed system allow-list is excluded (root, mysql.session, mysql.sys, mysql.infoschema, mariadb.sys, mysql, debian-sys-maint, percona.telemetry); anything else holding FILE is a FAIL, including a backup or ETL account that legitimately needs it. Whether file operations are actually usable depends on secure_file_priv, which is the separate MySQL File Import/Export check - this one looks purely at who holds the privilege.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
FAILAny non-system account holds FILEAn account outside the system list can read and write server files through SQL, so an injection against it becomes filesystem access.
PASSNoneOnly the built-in system accounts hold FILE, so no application login reaches the filesystem by this route.
SKIPNot accessibleNothing was verified. MySQL could not be queried, so the privilege table was never read and no account has been cleared.

Why it matters

FILE lets a SQL session read any file the mysqld OS user can read (LOAD DATA INFILE, LOAD_FILE()) and write new files anywhere it can write (SELECT ... INTO OUTFILE). With SQL injection this becomes arbitrary file read and, on web hosts, webshell write. The manual calls it "a global privilege that can be dangerous". OWASP: "disable the FILE privilege for all users". CIS MySQL "Ensure 'FILE' is not granted to non-administrative users".

Why it fails, and when it is wrong

  • GRANT ALL ON *.* includes FILE. Application accounts created that way are the usual cause.
  • Backup users for mysqldump --tab or LOAD DATA ETL jobs may legitimately need FILE; scope them with secure_file_priv (see the MySQL File Import/Export check) and a dedicated account that the web app does not use.
  • Super_priv and other global columns are not part of this check.

How to fix it

List the accounts holding FILE

The FAIL message names every non-system holder. From the server, the privilege table gives you the same list.

sudo mysql -N -e "SELECT CONCAT(user,'@',host) FROM mysql.user WHERE File_priv='Y'"

Revoke it

FILE is a global privilege, so it has to come off ON *.*; revoking it at database level reports success and changes nothing. Name the exact user@host pair, because a second row for the same user with a different host keeps the privilege.

REVOKE FILE ON *.* FROM 'app'@'%';

Restrict the file surface as well

A backup account running mysqldump --tab, or an ETL job using LOAD DATA, may genuinely need FILE. Confine it with secure_file_priv pointing at a dedicated directory, make sure the web application uses a different account, and record this one as an accepted exception. That setting is the separate MySQL File Import/Export check, not this one.

Confirm the grant is gone

SHOW GRANTS FOR 'app'@'%';

FILE should no longer appear anywhere in the returned grant list.

Verify the fix

sudo mysql -N -e "SELECT CONCAT(user,'@',host) FROM mysql.user WHERE File_priv='Y'"
# expected: only the system accounts (root, mysql.sys, debian-sys-maint, …)

sudo mysql -e "SHOW GRANTS FOR 'app'@'10.0.0.5'"
# expected: no FILE in the grant list

# Prove it from the application account itself:
mysql -u app -p -e "SELECT LOAD_FILE('/etc/passwd')"
# expected: NULL (or ERROR 1227 access denied)

Debugging

MYOK=0; see the Authentication debugging notes.

REVOKE FILE ON *.* FROM … must name the exact user@host pair; a second row for the same user with a different host keeps the privilege. Re-run the first query above to see every remaining holder.

The exclusion list is fixed and cannot be extended. Confine the damage with secure_file_priv pointing at a dedicated directory, make sure the web application uses a different account, and record this one as an accepted exception.

You revoked at the database level (ON appdb.*) while FILE is a global privilege; it must be revoked ON *.*. Check with SHOW GRANTS.

An account can reach files via other routes: SUPER/dynamic privileges, or a permissive secure_file_priv. See the MySQL Admin Accounts and MySQL File Import/Export checks.

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