From f6d172c03bf3cd71fd30ac9cc7b24fd64497b472 Mon Sep 17 00:00:00 2001 From: PranavKTiwari Date: Thu, 6 Aug 2026 13:47:33 +0530 Subject: [PATCH] MDEV-25515 User Account Host Names using CIDR notation CREATE USER u@'192.168.0.0/24'; -- same as '192.168.0.0/255.255.255.0' CIDR is an input spelling only, normalised to the netmask form while parsing. A malformed mask is now rejected instead of stored, and one already in the privilege tables is skipped at load with a warning instead of being honoured. Nothing rejected these before, and they are not merely useless: a non-contiguous mask such as 10.0.0.0/255.0.255.0 matches the scattered set 10.*0.*. Skipping rather than erroring keeps such a row droppable and renamable, except for a literal CIDR host stored by an older version (for example 10.0.0.0/24): the parser converts that name, so it can no longer be named and must be removed with a direct DELETE from the privilege tables. New error ER_INVALID_HOST_NETMASK, ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK --- mysql-test/main/host_cidr.result | 319 +++++++++++++++++++++++++++++++ mysql-test/main/host_cidr.test | 259 +++++++++++++++++++++++++ sql/share/errmsg-utf8.txt | 4 + sql/sql_acl.cc | 181 ++++++++++++++++++ sql/sql_acl.h | 1 + sql/sql_yacc.yy | 5 + 6 files changed, 769 insertions(+) create mode 100644 mysql-test/main/host_cidr.result create mode 100644 mysql-test/main/host_cidr.test diff --git a/mysql-test/main/host_cidr.result b/mysql-test/main/host_cidr.result new file mode 100644 index 00000000000..d5582373f81 --- /dev/null +++ b/mysql-test/main/host_cidr.result @@ -0,0 +1,319 @@ +# +# MDEV-25515 User Account Host Names using CIDR notation +# +call mtr.add_suppression("ignored, the host is not a valid ip/netmask"); +# CIDR is stored in the netmask form +CREATE USER c1@'127.0.0.0/8' IDENTIFIED BY 'pw'; +CREATE USER c2@'127.0.0.0/24' IDENTIFIED BY 'pw'; +CREATE USER c3@'127.0.0.1/32' IDENTIFIED BY 'pw'; +CREATE USER c4@'127.0.0.0/31' IDENTIFIED BY 'pw'; +CREATE USER c5@'0.0.0.0/1' IDENTIFIED BY 'pw'; +CREATE USER n1@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'pw'; +CREATE USER n2@'127.0.0.0/255.255.255.0' IDENTIFIED BY 'pw'; +SELECT User, Host FROM mysql.global_priv WHERE User LIKE 'c_' OR User LIKE 'n_' +ORDER BY User; +User Host +c1 127.0.0.0/255.0.0.0 +c2 127.0.0.0/255.255.255.0 +c3 127.0.0.1/255.255.255.255 +c4 127.0.0.0/255.255.255.254 +c5 0.0.0.0/128.0.0.0 +n1 127.0.0.0/255.0.0.0 +n2 127.0.0.0/255.255.255.0 +SHOW CREATE USER c1@'127.0.0.0/8'; +CREATE USER for c1@127.0.0.0/255.0.0.0 +CREATE USER `c1`@`127.0.0.0/255.0.0.0` IDENTIFIED BY PASSWORD '*D821809F681A40A6E379B50D0463EFAE20BDD122' +SHOW GRANTS FOR c2@'127.0.0.0/24'; +Grants for c2@127.0.0.0/255.255.255.0 +GRANT USAGE ON *.* TO `c2`@`127.0.0.0/255.255.255.0` IDENTIFIED BY PASSWORD '*D821809F681A40A6E379B50D0463EFAE20BDD122' +# Both spellings are the same account +CREATE USER dup@'127.0.0.0/8' IDENTIFIED BY 'pw'; +CREATE USER dup@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'other'; +ERROR HY000: Operation CREATE USER failed for 'dup'@'127.0.0.0/255.0.0.0' +CREATE USER IF NOT EXISTS dup@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'other'; +Warnings: +Note 1973 Can't create user 'dup'@'127.0.0.0/255.0.0.0'; it already exists +CREATE USER dup@'127.000.0.0/8' IDENTIFIED BY 'other'; +ERROR HY000: Operation CREATE USER failed for 'dup'@'127.0.0.0/255.0.0.0' +CREATE USER dup@'127.0.0.0/+8' IDENTIFIED BY 'other'; +ERROR HY000: Operation CREATE USER failed for 'dup'@'127.0.0.0/255.0.0.0' +CREATE USER dup@'127.0.0.0/ 8' IDENTIFIED BY 'other'; +ERROR HY000: Operation CREATE USER failed for 'dup'@'127.0.0.0/255.0.0.0' +CREATE USER dup@'127.0.0.0/255.000.0.0' IDENTIFIED BY 'other'; +ERROR HY000: Operation CREATE USER failed for 'dup'@'127.0.0.0/255.0.0.0' +SELECT User, Host FROM mysql.global_priv WHERE User='dup'; +User Host +dup 127.0.0.0/255.0.0.0 +DROP USER dup@'127.0.0.0/8'; +# Matching is unchanged +connect con1,127.0.0.1,c1,pw,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c1@127.0.0.0/255.0.0.0 +disconnect con1; +connection default; +connect con1,127.0.0.1,c2,pw,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c2@127.0.0.0/255.255.255.0 +disconnect con1; +connection default; +connect con1,127.0.0.1,c3,pw,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c3@127.0.0.1/255.255.255.255 +disconnect con1; +connection default; +connect con1,127.0.0.1,c4,pw,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c4@127.0.0.0/255.255.255.254 +disconnect con1; +connection default; +connect con1,127.0.0.1,c5,pw,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c5@0.0.0.0/128.0.0.0 +disconnect con1; +connection default; +connect con1,127.0.0.1,n1,pw,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +n1@127.0.0.0/255.0.0.0 +disconnect con1; +connection default; +CREATE USER z1@'127.0.0.0/32' IDENTIFIED BY 'pw'; +connect(127.0.0.1,z1,pw,test,MASTER_PORT,MASTER_SOCKET); +connect con1,127.0.0.1,z1,pw,,$MASTER_MYPORT,; +ERROR 28000: Access denied for user 'z1'@'localhost' (using password: YES) +DROP USER z1@'127.0.0.0/32'; +# Either spelling resolves to the same row, in every privilege table +CREATE DATABASE cidr_db; +CREATE TABLE cidr_db.t (a INT); +GRANT SELECT ON cidr_db.* TO c1@'127.0.0.0/255.0.0.0'; +SHOW GRANTS FOR c1@'127.0.0.0/8'; +Grants for c1@127.0.0.0/255.0.0.0 +GRANT USAGE ON *.* TO `c1`@`127.0.0.0/255.0.0.0` IDENTIFIED BY PASSWORD '*D821809F681A40A6E379B50D0463EFAE20BDD122' +GRANT SELECT ON `cidr_db`.* TO `c1`@`127.0.0.0/255.0.0.0` +GRANT INSERT ON cidr_db.* TO c1@'127.0.0.0/8'; +REVOKE SELECT ON cidr_db.* FROM c1@'127.0.0.0/8'; +SELECT User, Host FROM mysql.db WHERE User='c1'; +User Host +c1 127.0.0.0/255.0.0.0 +GRANT SELECT ON cidr_db.t TO c1@'127.0.0.0/8'; +SELECT User, Host FROM mysql.tables_priv WHERE User='c1'; +User Host +c1 127.0.0.0/255.0.0.0 +REVOKE SELECT ON cidr_db.t FROM c1@'127.0.0.0/255.0.0.0'; +SET PASSWORD FOR c1@'127.0.0.0/8' = PASSWORD('pw2'); +ALTER USER c1@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'pw'; +RENAME USER c1@'127.0.0.0/8' TO c1@'127.0.0.0/16'; +SELECT User, Host FROM mysql.global_priv WHERE User='c1'; +User Host +c1 127.0.0.0/255.255.0.0 +SELECT User, Host FROM mysql.db WHERE User='c1'; +User Host +c1 127.0.0.0/255.255.0.0 +RENAME USER c1@'127.0.0.0/255.255.0.0' TO c1@'127.0.0.0/8'; +REVOKE ALL PRIVILEGES, GRANT OPTION FROM c1@'127.0.0.0/255.0.0.0'; +SHOW GRANTS FOR c1@'127.0.0.0/8'; +Grants for c1@127.0.0.0/255.0.0.0 +GRANT USAGE ON *.* TO `c1`@`127.0.0.0/255.0.0.0` IDENTIFIED BY PASSWORD '*D821809F681A40A6E379B50D0463EFAE20BDD122' +DROP TABLE cidr_db.t; +DROP DATABASE cidr_db; +# GRANT normalises the host of an account it auto-creates +CREATE TABLE test.t (a INT); +CREATE PROCEDURE test.p1() SELECT 1; +CREATE ROLE cidr_role; +GRANT SELECT ON test.* TO g1@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT SELECT ON test.t TO g2@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT EXECUTE ON PROCEDURE test.p1 TO g3@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT cidr_role TO g4@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT PROXY ON ''@'%' TO g5@'10.0.0.0/24' IDENTIFIED BY 'pw'; +SELECT User, Host FROM mysql.global_priv WHERE User LIKE 'g_' ORDER BY User; +User Host +g1 10.0.0.0/255.255.255.0 +g2 10.0.0.0/255.255.255.0 +g3 10.0.0.0/255.255.255.0 +g4 10.0.0.0/255.255.255.0 +g5 10.0.0.0/255.255.255.0 +SELECT User, Host FROM mysql.db WHERE User='g1'; +User Host +g1 10.0.0.0/255.255.255.0 +SELECT User, Host FROM mysql.tables_priv WHERE User='g2'; +User Host +g2 10.0.0.0/255.255.255.0 +SELECT User, Host FROM mysql.procs_priv WHERE User='g3'; +User Host +g3 10.0.0.0/255.255.255.0 +SELECT User, Host FROM mysql.roles_mapping WHERE User='g4'; +User Host +g4 10.0.0.0/255.255.255.0 +SELECT User, Host FROM mysql.proxies_priv WHERE User='g5'; +User Host +g5 10.0.0.0/255.255.255.0 +DROP USER g1@'10.0.0.0/255.255.255.0', g2@'10.0.0.0/255.255.255.0', +g3@'10.0.0.0/255.255.255.0', g4@'10.0.0.0/255.255.255.0', +g5@'10.0.0.0/255.255.255.0'; +DROP ROLE cidr_role; +DROP PROCEDURE test.p1; +DROP TABLE test.t; +# DEFINER reaches the same grammar rule +CREATE USER def@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT ALL ON test.* TO def@'10.0.0.0/255.255.255.0'; +CREATE DEFINER=def@'10.0.0.0/24' VIEW test.v1 AS SELECT 1; +CREATE DEFINER=def@'10.0.0.0/24' PROCEDURE test.p2() SELECT 1; +SELECT definer FROM information_schema.views +WHERE table_schema='test' AND table_name='v1'; +definer +def@10.0.0.0/255.255.255.0 +SELECT definer FROM information_schema.routines +WHERE routine_schema='test' AND routine_name='p2'; +definer +def@10.0.0.0/255.255.255.0 +DROP VIEW test.v1; +DROP PROCEDURE test.p2; +DROP USER def@'10.0.0.0/255.255.255.0'; +# A host without '/' is not a masked host and is untouched +CREATE USER v1@'%', v2@'10.0.0.%', v3@'127.0.0._', v4@'localhost', +v5@'127.0.0.1', v6@'::1'; +SELECT User, Host FROM mysql.global_priv WHERE User LIKE 'v_' ORDER BY User; +User Host +v1 % +v2 10.0.0.% +v3 127.0.0._ +v4 localhost +v5 127.0.0.1 +v6 ::1 +DROP USER v1@'%', v2@'10.0.0.%', v3@'127.0.0._', v4@'localhost', +v5@'127.0.0.1', v6@'::1'; +# A malformed mask is rejected, quoting the host as it was written +CREATE USER e01@'10.0.0.0/33'; +ERROR HY000: Invalid masked host '10.0.0.0/33' for user 'e01' +CREATE USER e02@'10.0.0.0/0'; +ERROR HY000: Invalid masked host '10.0.0.0/0' for user 'e02' +CREATE USER e03@'10.0.0.0/-1'; +ERROR HY000: Invalid masked host '10.0.0.0/-1' for user 'e03' +CREATE USER e04@'10.0.0.0/0.0.0.0'; +ERROR HY000: Invalid masked host '10.0.0.0/0.0.0.0' for user 'e04' +CREATE USER e05@'10.0.0.0/255.0.255.0'; +ERROR HY000: Invalid masked host '10.0.0.0/255.0.255.0' for user 'e05' +CREATE USER e06@'10.0.0.0/255.255.0.255'; +ERROR HY000: Invalid masked host '10.0.0.0/255.255.0.255' for user 'e06' +CREATE USER e07@'10.1.2.3/24'; +ERROR HY000: Invalid masked host '10.1.2.3/24' for user 'e07' +CREATE USER e08@'10.1.2.3/255.255.255.0'; +ERROR HY000: Invalid masked host '10.1.2.3/255.255.255.0' for user 'e08' +CREATE USER e09@'127.0.0.0/1'; +ERROR HY000: Invalid masked host '127.0.0.0/1' for user 'e09' +CREATE USER e10@'2001:db8::/32'; +ERROR HY000: Invalid masked host '2001:db8::/32' for user 'e10' +CREATE USER e11@'10.0.0.%/24'; +ERROR HY000: Invalid masked host '10.0.0.%/24' for user 'e11' +CREATE USER e12@'10.0.0.0/abc'; +ERROR HY000: Invalid masked host '10.0.0.0/abc' for user 'e12' +CREATE USER e13@'10.0.0.0/'; +ERROR HY000: Invalid masked host '10.0.0.0/' for user 'e13' +CREATE USER e14@'10.0.0.0/24x'; +ERROR HY000: Invalid masked host '10.0.0.0/24x' for user 'e14' +GRANT SELECT ON test.* TO e20@'10.0.0.0/33' IDENTIFIED BY 'pw'; +ERROR HY000: Invalid masked host '10.0.0.0/33' for user 'e20' +GRANT SELECT ON test.* TO e21@'10.1.2.3/24' IDENTIFIED BY 'pw'; +ERROR HY000: Invalid masked host '10.1.2.3/24' for user 'e21' +SELECT User, Host FROM mysql.global_priv WHERE User LIKE 'e__'; +User Host +ALTER USER e30@'10.0.0.0/33' IDENTIFIED BY 'pw'; +ERROR HY000: Operation ALTER USER failed for 'e30'@'10.0.0.0/33' +REVOKE SELECT ON test.* FROM e30@'10.0.0.0/33'; +ERROR 42000: There is no such grant defined for user 'e30' on host '10.0.0.0/33' +# RENAME USER validates the target only +CREATE USER r1@'127.0.0.0/8' IDENTIFIED BY 'pw'; +RENAME USER r1@'127.0.0.0/8' TO r1@'10.1.2.3/24'; +ERROR HY000: Operation RENAME USER failed for 'r1'@'10.1.2.3/24' +RENAME USER r1@'127.0.0.0/8' TO r1@'10.0.0.0/255.0.255.0'; +ERROR HY000: Operation RENAME USER failed for 'r1'@'10.0.0.0/255.0.255.0' +SHOW WARNINGS; +Level Code Message +Warning 4274 Invalid masked host '10.0.0.0/255.0.255.0' for user 'r1' +Error 1396 Operation RENAME USER failed for 'r1'@'10.0.0.0/255.0.255.0' +RENAME USER r1@'127.0.0.0/8' TO r1@'10.0.0.0/16'; +SELECT User, Host FROM mysql.global_priv WHERE User='r1'; +User Host +r1 10.0.0.0/255.255.0.0 +DROP USER r1@'10.0.0.0/255.255.0.0'; +# acl_load() skips a stored row whose mask it cannot honour +CREATE USER leg@'10.0.0.0/24' IDENTIFIED BY 'pw'; +UPDATE mysql.global_priv SET Host='10.0.0.0/255.0.255.0' WHERE User='leg'; +FLUSH PRIVILEGES; +SHOW GRANTS FOR leg@'10.0.0.0/255.0.255.0'; +ERROR 42000: There is no such grant defined for user 'leg' on host '10.0.0.0/255.0.255.0' +RENAME USER leg@'10.0.0.0/255.0.255.0' TO leg@'10.0.0.0/16'; +FLUSH PRIVILEGES; +SHOW GRANTS FOR leg@'10.0.0.0/255.255.0.0'; +Grants for leg@10.0.0.0/255.255.0.0 +GRANT USAGE ON *.* TO `leg`@`10.0.0.0/255.255.0.0` IDENTIFIED BY PASSWORD '*D821809F681A40A6E379B50D0463EFAE20BDD122' +DROP USER leg@'10.0.0.0/255.255.0.0'; +CREATE USER leg2@'10.0.0.0/24' IDENTIFIED BY 'pw'; +UPDATE mysql.global_priv SET Host='10.1.2.3/255.255.255.0' WHERE User='leg2'; +FLUSH PRIVILEGES; +DROP USER leg2@'10.1.2.3/255.255.255.0'; +SELECT User, Host FROM mysql.global_priv WHERE User='leg2'; +User Host +CREATE USER leg3@'10.0.0.0/24' IDENTIFIED BY 'pw'; +UPDATE mysql.global_priv SET Host='10.0.0.0/24' WHERE User='leg3'; +FLUSH PRIVILEGES; +SHOW GRANTS FOR leg3@'10.0.0.0/24'; +ERROR 42000: There is no such grant defined for user 'leg3' on host '10.0.0.0/255.255.255.0' +DROP USER leg3@'10.0.0.0/24'; +ERROR HY000: Operation DROP USER failed for 'leg3'@'10.0.0.0/255.255.255.0' +DELETE FROM mysql.global_priv WHERE User='leg3'; +FLUSH PRIVILEGES; +# ... in tables_priv and procs_priv too +CREATE DATABASE legdb; +CREATE TABLE legdb.t (a INT); +CREATE PROCEDURE legdb.p() SELECT 1; +CREATE USER legt@'%' IDENTIFIED BY 'pw'; +GRANT SELECT ON legdb.t TO legt@'%'; +GRANT EXECUTE ON PROCEDURE legdb.p TO legt@'%'; +connect con1,127.0.0.1,legt,pw,,$MASTER_MYPORT,; +connection con1; +SELECT * FROM legdb.t; +a +CALL legdb.p(); +1 +1 +disconnect con1; +connection default; +UPDATE mysql.tables_priv SET Host='127.0.0.0/255.0.255.0' WHERE User='legt'; +UPDATE mysql.procs_priv SET Host='127.0.0.0/255.0.255.0' WHERE User='legt'; +FLUSH PRIVILEGES; +connect con1,127.0.0.1,legt,pw,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +legt@% +SELECT * FROM legdb.t; +ERROR 42000: SELECT command denied to user 'legt'@'localhost' for table `legdb`.`t` +CALL legdb.p(); +ERROR 42000: execute command denied to user 'legt'@'%' for routine 'legdb.p' +disconnect con1; +connection default; +DELETE FROM mysql.tables_priv WHERE User='legt'; +DELETE FROM mysql.procs_priv WHERE User='legt'; +DROP USER legt@'%'; +DROP PROCEDURE legdb.p; +DROP TABLE legdb.t; +DROP DATABASE legdb; +DROP USER c1@'127.0.0.0/8', c2@'127.0.0.0/24', c3@'127.0.0.1/32', +c4@'127.0.0.0/31', c5@'0.0.0.0/1', +n1@'127.0.0.0/255.0.0.0', n2@'127.0.0.0/255.255.255.0'; +FLUSH PRIVILEGES; +# +# End of MDEV-25515 tests +# diff --git a/mysql-test/main/host_cidr.test b/mysql-test/main/host_cidr.test new file mode 100644 index 00000000000..ff8d3f10624 --- /dev/null +++ b/mysql-test/main/host_cidr.test @@ -0,0 +1,259 @@ +--source include/not_embedded.inc + +--echo # +--echo # MDEV-25515 User Account Host Names using CIDR notation +--echo # + +call mtr.add_suppression("ignored, the host is not a valid ip/netmask"); + +--echo # CIDR is stored in the netmask form +CREATE USER c1@'127.0.0.0/8' IDENTIFIED BY 'pw'; +CREATE USER c2@'127.0.0.0/24' IDENTIFIED BY 'pw'; +CREATE USER c3@'127.0.0.1/32' IDENTIFIED BY 'pw'; +CREATE USER c4@'127.0.0.0/31' IDENTIFIED BY 'pw'; +CREATE USER c5@'0.0.0.0/1' IDENTIFIED BY 'pw'; +CREATE USER n1@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'pw'; +CREATE USER n2@'127.0.0.0/255.255.255.0' IDENTIFIED BY 'pw'; +SELECT User, Host FROM mysql.global_priv WHERE User LIKE 'c_' OR User LIKE 'n_' +ORDER BY User; +SHOW CREATE USER c1@'127.0.0.0/8'; +SHOW GRANTS FOR c2@'127.0.0.0/24'; + +--echo # Both spellings are the same account +CREATE USER dup@'127.0.0.0/8' IDENTIFIED BY 'pw'; +--error ER_CANNOT_USER +CREATE USER dup@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'other'; +CREATE USER IF NOT EXISTS dup@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'other'; + +--error ER_CANNOT_USER +CREATE USER dup@'127.000.0.0/8' IDENTIFIED BY 'other'; +--error ER_CANNOT_USER +CREATE USER dup@'127.0.0.0/+8' IDENTIFIED BY 'other'; +--error ER_CANNOT_USER +CREATE USER dup@'127.0.0.0/ 8' IDENTIFIED BY 'other'; +--error ER_CANNOT_USER +CREATE USER dup@'127.0.0.0/255.000.0.0' IDENTIFIED BY 'other'; +SELECT User, Host FROM mysql.global_priv WHERE User='dup'; +DROP USER dup@'127.0.0.0/8'; + +--echo # Matching is unchanged +connect (con1,127.0.0.1,c1,pw,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; +connect (con1,127.0.0.1,c2,pw,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; +connect (con1,127.0.0.1,c3,pw,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; +connect (con1,127.0.0.1,c4,pw,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; +connect (con1,127.0.0.1,c5,pw,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; +connect (con1,127.0.0.1,n1,pw,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +CREATE USER z1@'127.0.0.0/32' IDENTIFIED BY 'pw'; +--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT +--error ER_ACCESS_DENIED_ERROR +connect (con1,127.0.0.1,z1,pw,,$MASTER_MYPORT,); +DROP USER z1@'127.0.0.0/32'; + + +--echo # Either spelling resolves to the same row, in every privilege table +CREATE DATABASE cidr_db; +CREATE TABLE cidr_db.t (a INT); +GRANT SELECT ON cidr_db.* TO c1@'127.0.0.0/255.0.0.0'; +SHOW GRANTS FOR c1@'127.0.0.0/8'; +GRANT INSERT ON cidr_db.* TO c1@'127.0.0.0/8'; +REVOKE SELECT ON cidr_db.* FROM c1@'127.0.0.0/8'; +SELECT User, Host FROM mysql.db WHERE User='c1'; +GRANT SELECT ON cidr_db.t TO c1@'127.0.0.0/8'; +SELECT User, Host FROM mysql.tables_priv WHERE User='c1'; +REVOKE SELECT ON cidr_db.t FROM c1@'127.0.0.0/255.0.0.0'; +SET PASSWORD FOR c1@'127.0.0.0/8' = PASSWORD('pw2'); +ALTER USER c1@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'pw'; +RENAME USER c1@'127.0.0.0/8' TO c1@'127.0.0.0/16'; +SELECT User, Host FROM mysql.global_priv WHERE User='c1'; +SELECT User, Host FROM mysql.db WHERE User='c1'; +RENAME USER c1@'127.0.0.0/255.255.0.0' TO c1@'127.0.0.0/8'; +REVOKE ALL PRIVILEGES, GRANT OPTION FROM c1@'127.0.0.0/255.0.0.0'; +SHOW GRANTS FOR c1@'127.0.0.0/8'; +DROP TABLE cidr_db.t; +DROP DATABASE cidr_db; + +--echo # GRANT normalises the host of an account it auto-creates +CREATE TABLE test.t (a INT); +CREATE PROCEDURE test.p1() SELECT 1; +CREATE ROLE cidr_role; +GRANT SELECT ON test.* TO g1@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT SELECT ON test.t TO g2@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT EXECUTE ON PROCEDURE test.p1 TO g3@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT cidr_role TO g4@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT PROXY ON ''@'%' TO g5@'10.0.0.0/24' IDENTIFIED BY 'pw'; +SELECT User, Host FROM mysql.global_priv WHERE User LIKE 'g_' ORDER BY User; +SELECT User, Host FROM mysql.db WHERE User='g1'; +SELECT User, Host FROM mysql.tables_priv WHERE User='g2'; +SELECT User, Host FROM mysql.procs_priv WHERE User='g3'; +SELECT User, Host FROM mysql.roles_mapping WHERE User='g4'; +SELECT User, Host FROM mysql.proxies_priv WHERE User='g5'; +DROP USER g1@'10.0.0.0/255.255.255.0', g2@'10.0.0.0/255.255.255.0', + g3@'10.0.0.0/255.255.255.0', g4@'10.0.0.0/255.255.255.0', + g5@'10.0.0.0/255.255.255.0'; +DROP ROLE cidr_role; +DROP PROCEDURE test.p1; +DROP TABLE test.t; + +--echo # DEFINER reaches the same grammar rule +CREATE USER def@'10.0.0.0/24' IDENTIFIED BY 'pw'; +GRANT ALL ON test.* TO def@'10.0.0.0/255.255.255.0'; +CREATE DEFINER=def@'10.0.0.0/24' VIEW test.v1 AS SELECT 1; +CREATE DEFINER=def@'10.0.0.0/24' PROCEDURE test.p2() SELECT 1; +SELECT definer FROM information_schema.views + WHERE table_schema='test' AND table_name='v1'; +SELECT definer FROM information_schema.routines + WHERE routine_schema='test' AND routine_name='p2'; +DROP VIEW test.v1; +DROP PROCEDURE test.p2; +DROP USER def@'10.0.0.0/255.255.255.0'; + +--echo # A host without '/' is not a masked host and is untouched +CREATE USER v1@'%', v2@'10.0.0.%', v3@'127.0.0._', v4@'localhost', + v5@'127.0.0.1', v6@'::1'; +SELECT User, Host FROM mysql.global_priv WHERE User LIKE 'v_' ORDER BY User; +DROP USER v1@'%', v2@'10.0.0.%', v3@'127.0.0._', v4@'localhost', + v5@'127.0.0.1', v6@'::1'; + +--echo # A malformed mask is rejected, quoting the host as it was written +--error ER_INVALID_HOST_NETMASK +CREATE USER e01@'10.0.0.0/33'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e02@'10.0.0.0/0'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e03@'10.0.0.0/-1'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e04@'10.0.0.0/0.0.0.0'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e05@'10.0.0.0/255.0.255.0'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e06@'10.0.0.0/255.255.0.255'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e07@'10.1.2.3/24'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e08@'10.1.2.3/255.255.255.0'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e09@'127.0.0.0/1'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e10@'2001:db8::/32'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e11@'10.0.0.%/24'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e12@'10.0.0.0/abc'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e13@'10.0.0.0/'; +--error ER_INVALID_HOST_NETMASK +CREATE USER e14@'10.0.0.0/24x'; +--error ER_INVALID_HOST_NETMASK +GRANT SELECT ON test.* TO e20@'10.0.0.0/33' IDENTIFIED BY 'pw'; +--error ER_INVALID_HOST_NETMASK +GRANT SELECT ON test.* TO e21@'10.1.2.3/24' IDENTIFIED BY 'pw'; +SELECT User, Host FROM mysql.global_priv WHERE User LIKE 'e__'; + +--error ER_CANNOT_USER +ALTER USER e30@'10.0.0.0/33' IDENTIFIED BY 'pw'; +--error ER_NONEXISTING_GRANT +REVOKE SELECT ON test.* FROM e30@'10.0.0.0/33'; + +--echo # RENAME USER validates the target only +CREATE USER r1@'127.0.0.0/8' IDENTIFIED BY 'pw'; +--error ER_CANNOT_USER +RENAME USER r1@'127.0.0.0/8' TO r1@'10.1.2.3/24'; +--error ER_CANNOT_USER +RENAME USER r1@'127.0.0.0/8' TO r1@'10.0.0.0/255.0.255.0'; +SHOW WARNINGS; +RENAME USER r1@'127.0.0.0/8' TO r1@'10.0.0.0/16'; +SELECT User, Host FROM mysql.global_priv WHERE User='r1'; +DROP USER r1@'10.0.0.0/255.255.0.0'; + +--echo # acl_load() skips a stored row whose mask it cannot honour +CREATE USER leg@'10.0.0.0/24' IDENTIFIED BY 'pw'; +UPDATE mysql.global_priv SET Host='10.0.0.0/255.0.255.0' WHERE User='leg'; +FLUSH PRIVILEGES; +--error ER_NONEXISTING_GRANT +SHOW GRANTS FOR leg@'10.0.0.0/255.0.255.0'; +RENAME USER leg@'10.0.0.0/255.0.255.0' TO leg@'10.0.0.0/16'; +FLUSH PRIVILEGES; +SHOW GRANTS FOR leg@'10.0.0.0/255.255.0.0'; +DROP USER leg@'10.0.0.0/255.255.0.0'; + +CREATE USER leg2@'10.0.0.0/24' IDENTIFIED BY 'pw'; +UPDATE mysql.global_priv SET Host='10.1.2.3/255.255.255.0' WHERE User='leg2'; +FLUSH PRIVILEGES; +DROP USER leg2@'10.1.2.3/255.255.255.0'; +SELECT User, Host FROM mysql.global_priv WHERE User='leg2'; + +CREATE USER leg3@'10.0.0.0/24' IDENTIFIED BY 'pw'; +UPDATE mysql.global_priv SET Host='10.0.0.0/24' WHERE User='leg3'; +FLUSH PRIVILEGES; +--error ER_NONEXISTING_GRANT +SHOW GRANTS FOR leg3@'10.0.0.0/24'; +--error ER_CANNOT_USER +DROP USER leg3@'10.0.0.0/24'; +DELETE FROM mysql.global_priv WHERE User='leg3'; +FLUSH PRIVILEGES; + +--echo # ... in tables_priv and procs_priv too +CREATE DATABASE legdb; +CREATE TABLE legdb.t (a INT); +CREATE PROCEDURE legdb.p() SELECT 1; +CREATE USER legt@'%' IDENTIFIED BY 'pw'; +GRANT SELECT ON legdb.t TO legt@'%'; +GRANT EXECUTE ON PROCEDURE legdb.p TO legt@'%'; +connect (con1,127.0.0.1,legt,pw,,$MASTER_MYPORT,); +connection con1; +SELECT * FROM legdb.t; +CALL legdb.p(); +disconnect con1; +connection default; +UPDATE mysql.tables_priv SET Host='127.0.0.0/255.0.255.0' WHERE User='legt'; +UPDATE mysql.procs_priv SET Host='127.0.0.0/255.0.255.0' WHERE User='legt'; +FLUSH PRIVILEGES; +connect (con1,127.0.0.1,legt,pw,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +--error ER_TABLEACCESS_DENIED_ERROR +SELECT * FROM legdb.t; +--error ER_PROCACCESS_DENIED_ERROR +CALL legdb.p(); +disconnect con1; +connection default; +DELETE FROM mysql.tables_priv WHERE User='legt'; +DELETE FROM mysql.procs_priv WHERE User='legt'; +DROP USER legt@'%'; +DROP PROCEDURE legdb.p; +DROP TABLE legdb.t; +DROP DATABASE legdb; + +DROP USER c1@'127.0.0.0/8', c2@'127.0.0.0/24', c3@'127.0.0.1/32', + c4@'127.0.0.0/31', c5@'0.0.0.0/1', + n1@'127.0.0.0/255.0.0.0', n2@'127.0.0.0/255.255.255.0'; +FLUSH PRIVILEGES; + +--echo # +--echo # End of MDEV-25515 tests +--echo # diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index 800ca734107..3bc85fefad8 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -12423,3 +12423,7 @@ ER_RESULT_CUT_BY_LIMIT eng "Result was cut by %s) because of %s limit" ER_CUT_VALUES_WHILE_PROCESSING eng "Some values were cut while processing %s). Increase %s if you want to avoid the cut" +ER_INVALID_HOST_NETMASK + eng "Invalid masked host '%s' for user '%s'" +ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK + eng "'%s' entry '%s' ignored, the host is not a valid ip/netmask" diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 597dbce0f1d..52be716f02c 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -339,6 +339,7 @@ ulong role_global_merges= 0, role_db_merges= 0, role_table_merges= 0, #ifndef NO_EMBEDDED_ACCESS_CHECKS static bool ignore_max_password_errors(const ACL_USER *acl_user); static void update_hostname(acl_host_and_ip *host, const char *hostname); +static bool valid_host_mask(const char *hostname); static bool show_proxy_grants (THD *, const char *, const char *, char *, size_t); static bool show_role_grants(THD *, const char *, @@ -443,6 +444,17 @@ public: safe_str(host.hostname)); return TRUE; } + if (!valid_host_mask(host.hostname) || + !valid_host_mask(proxied_host.hostname)) + { + char entry[2 * (USERNAME_LENGTH + HOSTNAME_LENGTH) + 4]; + my_snprintf(entry, sizeof(entry), "%s@%s %s@%s", proxied_user, + safe_str(proxied_host.hostname), user, + safe_str(host.hostname)); + sql_print_warning(ER_DEFAULT(ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK), + "proxies_priv", entry); + return TRUE; + } return FALSE; } @@ -3437,6 +3449,15 @@ static bool acl_load(THD *thd, const Grant_tables& tables) host.host.hostname, host.db); continue; } + if (!valid_host_mask(host.host.hostname)) + { + char entry[HOSTNAME_LENGTH + NAME_LEN + 2]; + my_snprintf(entry, sizeof(entry), "%s|%s", host.host.hostname, + safe_str(host.db)); + sql_print_warning(ER_DEFAULT(ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK), + "host", entry); + continue; + } #ifndef TO_BE_REMOVED if (host_table.num_fields() == 8) { // Without grant @@ -3518,6 +3539,17 @@ static bool acl_load(THD *thd, const Grant_tables& tables) continue; } + /* Skipped rows stay in the table, so DROP and RENAME USER still work */ + if (!valid_host_mask(user.host.hostname)) + { + char entry[USERNAME_LENGTH + HOSTNAME_LENGTH + 2]; + my_snprintf(entry, sizeof(entry), "%s@%s", user.user.str, + safe_str(user.host.hostname)); + sql_print_warning(ER_DEFAULT(ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK), + "user", entry); + continue; + } + if (user_table.get_auth(thd, &acl_memroot, &user)) continue; for (uint i= 0; i < user.nauth; i++) @@ -3592,6 +3624,15 @@ static bool acl_load(THD *thd, const Grant_tables& tables) db.db, db.user, safe_str(db.host.hostname)); continue; } + if (!valid_host_mask(db.host.hostname)) + { + char entry[NAME_LEN + USERNAME_LENGTH + HOSTNAME_LENGTH + 3]; + my_snprintf(entry, sizeof(entry), "%s %s@%s", db.db, + db.user, safe_str(db.host.hostname)); + sql_print_warning(ER_DEFAULT(ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK), + "db", entry); + continue; + } db.access= access_t(fix_rights_for_db(db_table.get_access())); db.initial_access= db.access; db.sort=get_magic_sort("hdu", db.host.hostname, db.db, db.user); @@ -5407,6 +5448,101 @@ static const char *calc_ip(const char *ip, long *val, char end) return ip; } +/* Convert CIDR prefix length (1-32) to a 32-bit IPv4 subnet mask. */ +static const char *calc_cidr(const char *prefix_str, long *val) +{ + uchar prefix; + + if (!(prefix_str=str2int(prefix_str, INT16_MAX, 10, &prefix)) || + *prefix_str != '\0' || !prefix || prefix > 32) + return 0; + + *val= (long) (uint32) (0xFFFFFFFFU << (32 - prefix)); + return prefix_str; +} + +/* + compare_hostname() tests (client_ip & ip_mask) == ip, so a zero or + non-contiguous mask, or an ip with host bits set, can never match. +*/ +static bool valid_masked_ip(long ip, long mask) +{ + ulong inv, m= (ulong) (uint32) mask; + + if (!m) + return false; /* a mask of 0.0.0.0 */ + + inv= (~m) & 0xFFFFFFFFUL; + if (inv & (inv + 1)) + return false; /* not contiguous */ + + return !(((ulong) (uint32) ip) & inv); +} + + +/* + Rewrite a masked host into the canonical netmask spelling: + + 10.0.0.0/8 -> 10.0.0.0/255.0.0.0 + 010.0.0.0/+8 -> 10.0.0.0/255.0.0.0 + 10.0.0.0/255.0.0.0 -> unchanged, already canonical + + Anything that is not a valid masked host is returned unchanged and no error + is raised, so DROP USER and RENAME USER can still name a malformed host + stored by a version that did not validate it. + + The exception is a stored literal CIDR host such as 10.0.0.0/24: it is + converted here, so it can no longer be named and has to be removed with a + direct DELETE from the privilege tables. +*/ + +LEX_CSTRING normalize_masked_host(THD *thd, const LEX_CSTRING &host) +{ + long ip, mask; + const char *p; + char buf[32]; /* 255.255.255.255/255.255.255.255 and NUL */ + uint32 a, m; + size_t len; + + if (!host.str || !memchr(host.str, '/', host.length)) + return host; + if (!(p= calc_ip(host.str, &ip, '/'))) + return host; + if (!calc_ip(p + 1, &mask, '\0') && !calc_cidr(p + 1, &mask)) + return host; + if (!valid_masked_ip(ip, mask)) + return host; + + a= (uint32) ip; + m= (uint32) mask; + len= my_snprintf(buf, sizeof(buf), "%u.%u.%u.%u/%u.%u.%u.%u", + a >> 24, (a >> 16) & 0xFF, (a >> 8) & 0xFF, a & 0xFF, + m >> 24, (m >> 16) & 0xFF, (m >> 8) & 0xFF, m & 0xFF); + + return thd->strmake_lex_cstring(buf, len); +} + + +/* + Check that a host given in masked form is well formed: + a.b.c.d/255.255.255.0 + + A CIDR prefix is rejected here on purpose: normalize_masked_host() has + already rewritten every valid one, so a prefix reaching this point means + the host never went through the parser. +*/ + +static bool valid_host_mask(const char *hostname) +{ + long ip, mask; + const char *p; + + if (!hostname || !strchr(hostname, '/')) + return true; /* not a masked host */ + + return (p= calc_ip(hostname, &ip, '/')) && + calc_ip(p + 1, &mask, '\0') && valid_masked_ip(ip, mask); +} static void update_hostname(acl_host_and_ip *host, const char *hostname) { @@ -5666,6 +5802,14 @@ static int replace_user_table(THD *thd, const User_table &user_table, if (!combo->auth) combo->auth= &auth_no_password; + /* Only rows about to be created, so an older bad row stays droppable */ + if (!valid_host_mask(combo->host.str)) + { + my_error(ER_INVALID_HOST_NETMASK, MYF(0), combo->host.str, + combo->user.str); + goto end; + } + old_row_exists = 0; restore_record(table, s->default_values); user_table.set_host(combo->host.str, combo->host.length); @@ -9680,6 +9824,17 @@ static bool grant_load(THD *thd, } } + if (!valid_host_mask(mem_check->host.hostname)) + { + char entry[NAME_LEN + USERNAME_LENGTH + HOSTNAME_LENGTH + 3]; + my_snprintf(entry, sizeof(entry), "%s %s@%s", safe_str(mem_check->tname), + mem_check->user, safe_str(mem_check->host.hostname)); + sql_print_warning(ER_DEFAULT(ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK), + "tables_priv", entry); + delete mem_check; + continue; + } + if (! mem_check->ok()) delete mem_check; else if (column_priv_insert(mem_check)) @@ -9723,6 +9878,18 @@ static bool grant_load(THD *thd, continue; } } + + if (!valid_host_mask(mem_check->host.hostname)) + { + char entry[NAME_LEN + USERNAME_LENGTH + HOSTNAME_LENGTH + 3]; + my_snprintf(entry, sizeof(entry), "%s %s@%s", + safe_str(mem_check->tname), mem_check->user, + safe_str(mem_check->host.hostname)); + sql_print_warning(ER_DEFAULT(ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK), + "procs_priv", entry); + delete mem_check; + continue; + } enum_sp_type type= (enum_sp_type)procs_priv.routine_type()->val_int(); const Sp_handler *sph= Sp_handler::handler(type); if (!sph || !(hash= sph->get_priv_hash())) @@ -13332,6 +13499,18 @@ bool mysql_rename_user(THD *thd, List &list) DBUG_ASSERT(!user_from->is_role()); DBUG_ASSERT(!user_to->is_role()); + /* Target only, so a bad host can be repaired by renaming it */ + if (!valid_host_mask(user_to->host.str)) + { + push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, + ER_INVALID_HOST_NETMASK, + ER_THD(thd, ER_INVALID_HOST_NETMASK), + user_to->host.str, user_to->user.str); + append_user(thd, &wrong_users, user_to); + result= TRUE; + continue; + } + /* Search all in-memory structures and grant tables for a mention of the new user name. @@ -14234,6 +14413,8 @@ access_t get_column_grant(THD *, GRANT_INFO *, const char *, const char *, { return access_t(ALL_KNOWN_ACL); } int acl_check_setrole(THD *, const LEX_CSTRING &, access_t *) { return 0; } int acl_setrole(THD *, const LEX_CSTRING &, const access_t&) { return 0; } +LEX_CSTRING normalize_masked_host(THD *, const LEX_CSTRING &host) +{ return host; } #endif /*NO_EMBEDDED_ACCESS_CHECKS */ static int set_privs_on_login(THD *thd, const ACL_USER *acl_user) diff --git a/sql/sql_acl.h b/sql/sql_acl.h index 753c52bc491..3f1537da821 100644 --- a/sql/sql_acl.h +++ b/sql/sql_acl.h @@ -148,6 +148,7 @@ bool mysql_drop_user(THD *thd, List &list, bool handle_as_role); bool mysql_rename_user(THD *thd, List &list); int mysql_alter_user(THD *thd, List &list); bool mysql_revoke_all(THD *thd, List &list); +LEX_CSTRING normalize_masked_host(THD *thd, const LEX_CSTRING &host); void fill_effective_table_privileges(THD *thd, GRANT_INFO *grant, const char *db, const char *table); bool sp_revoke_privileges(THD *thd, diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 0ba1803749f..4ec4932c950 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -16824,7 +16824,12 @@ user_name: check_host_name(&$$->host)) MYSQL_YYABORT; if ($$->host.str[0]) + { $$->host= thd->make_ident_casedn($$->host); + $$->host= normalize_masked_host(thd, $$->host); + if (unlikely(!$$->host.str)) + MYSQL_YYABORT; + } else $$->host= host_not_specified; }