From a7f376cffd51b4cdec64ac1ee9fb27ec14c15057 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 Accept CIDR notation (RFC 4632) as an alternative spelling of the ip/netmask host form, matching MySQL 8.0.23 (WL#14074): CREATE USER u@'192.168.0.0/24'; -- same as '192.168.0.0/255.255.255.0' update_hostname() tries the dotted-quad mask first and falls back to the new calc_cidr(), so both spellings yield the same acl_host_and_ip and are interchangeable wherever a host is used - user, db, table, routine and proxy privileges, and the plugin API. IPv4 only. The host string is stored and reported verbatim; neither spelling is normalised. Add is_valid_masked_host(), rejecting at DDL time what was previously accepted and left silently unusable: - a prefix outside 1..32, or a mask of 0.0.0.0 - a non-contiguous mask, e.g. 10.0.0.0/255.0.255.0 - an address with host bits set, e.g. 10.1.2.3/24 - anything containing '/' that is not a dotted quad, including IPv6 prefixes such as 2001:db8::/32 The check runs in replace_user_table(), covering CREATE USER and every GRANT variant that can auto-create an account, and in mysql_rename_user() for the rename target. It applies only to rows about to be created: - existing rows are not validated, so an account created by an older version stays revocable, renamable and droppable - acl_load() is unchanged, so a malformed row in the privilege tables cannot prevent the server from starting - RENAME USER validates the target only, so such an account can be repaired by renaming it onto a valid host New error ER_INVALID_HOST_NETMASK. acl_user_compare() now compares ip_mask before the host string, treating an unmasked host as /32, so the most specific subnet wins: u@'10.0.0.0/24' is preferred over u@'10.0.0.0/8' This corrects precedence for existing netmask accounts as well, and applies to acl_users only; acl_dbs, acl_hosts, acl_proxy_users and the table/routine grant hashes are left unchanged. --- mysql-test/main/grant_cidr.result | 232 ++++++++++++++++++++++++++++ mysql-test/main/grant_cidr.test | 242 ++++++++++++++++++++++++++++++ sql/share/errmsg-utf8.txt | 2 + sql/sql_acl.cc | 91 ++++++++++- 4 files changed, 564 insertions(+), 3 deletions(-) create mode 100644 mysql-test/main/grant_cidr.result create mode 100644 mysql-test/main/grant_cidr.test diff --git a/mysql-test/main/grant_cidr.result b/mysql-test/main/grant_cidr.result new file mode 100644 index 0000000000000..b5ecc7d1f78c6 --- /dev/null +++ b/mysql-test/main/grant_cidr.result @@ -0,0 +1,232 @@ +# Valid CIDR prefixes +CREATE USER c01@'127.0.0.0/8' IDENTIFIED BY 'p'; +CREATE USER c02@'127.0.0.0/24' IDENTIFIED BY 'p'; +CREATE USER c03@'127.0.0.1/32' IDENTIFIED BY 'p'; +CREATE USER c18@'127.0.0.0/31' IDENTIFIED BY 'p'; +CREATE USER c19@'127.0.0.0/30' IDENTIFIED BY 'p'; +CREATE USER c06@'0.0.0.0/1' IDENTIFIED BY 'p'; +connect con1,127.0.0.1,c01,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c01@127.0.0.0/8 +disconnect con1; +connection default; +connect con1,127.0.0.1,c02,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c02@127.0.0.0/24 +disconnect con1; +connection default; +connect con1,127.0.0.1,c03,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c03@127.0.0.1/32 +disconnect con1; +connection default; +connect con1,127.0.0.1,c18,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c18@127.0.0.0/31 +disconnect con1; +connection default; +connect con1,127.0.0.1,c19,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c19@127.0.0.0/30 +disconnect con1; +connection default; +connect con1,127.0.0.1,c06,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c06@0.0.0.0/1 +disconnect con1; +connection default; +# Malformed masks are rejected at CREATE USER. +CREATE USER c08@'127.0.0.0/0' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.0/0' for user 'c08' +CREATE USER c09@'127.0.0.1/0' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.1/0' for user 'c09' +CREATE USER c10@'127.0.0.0/33' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.0/33' for user 'c10' +CREATE USER c11@'127.0.0.0/-1' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.0/-1' for user 'c11' +CREATE USER c12@'127.0.0.0/abc' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.0/abc' for user 'c12' +CREATE USER c13@'127.0.0.0/' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.0/' for user 'c13' +CREATE USER c14@'127.0.0.0/24x' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.0/24x' for user 'c14' +# CIDR == netmask parity +CREATE USER n01@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'p'; +CREATE USER n02@'127.0.0.0/255.255.255.0' IDENTIFIED BY 'p'; +CREATE USER n03@'127.0.0.1/255.255.255.255' IDENTIFIED BY 'p'; +CREATE USER n04@'127.0.0.0/255.255.255.255' IDENTIFIED BY 'p'; +CREATE USER n05@'127.0.0.0/0.0.0.0' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.0/0.0.0.0' for user 'n05' +CREATE USER c04@'127.0.0.0/32' IDENTIFIED BY 'p'; +connect con1,127.0.0.1,n01,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +n01@127.0.0.0/255.0.0.0 +disconnect con1; +connection default; +connect con1,127.0.0.1,n02,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +n02@127.0.0.0/255.255.255.0 +disconnect con1; +connection default; +connect con1,127.0.0.1,n03,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +n03@127.0.0.1/255.255.255.255 +disconnect con1; +connection default; +connect(127.0.0.1,n04,p,test,MASTER_PORT,MASTER_SOCKET); +connect con1,127.0.0.1,n04,p,,$MASTER_MYPORT,; +ERROR 28000: Access denied for user 'n04'@'localhost' (using password: YES) +connect(127.0.0.1,c04,p,test,MASTER_PORT,MASTER_SOCKET); +connect con1,127.0.0.1,c04,p,,$MASTER_MYPORT,; +ERROR 28000: Access denied for user 'c04'@'localhost' (using password: YES) +# The address must be the network address (no host bits set). +CREATE USER c05@'127.0.0.0/1' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.0/1' for user 'c05' +CREATE USER c07@'127.0.0.1/24' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.1/24' for user 'c07' +CREATE USER n07@'127.0.0.1/255.255.255.0' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '127.0.0.1/255.255.255.0' for user 'n07' +# host specificity ordering +CREATE USER ord1@'127.0.0.0/8' IDENTIFIED BY 'p'; +CREATE USER ord1@'127.0.0.0/24' IDENTIFIED BY 'p'; +CREATE USER ord2@'127.0.0.0/8' IDENTIFIED BY 'p'; +CREATE USER ord2@'127.0.0.0/16' IDENTIFIED BY 'p'; +CREATE USER ord3@'127.0.0.0/9' IDENTIFIED BY 'p'; +CREATE USER ord3@'127.0.0.1/32' IDENTIFIED BY 'p'; +CREATE USER ord4@'127.0.0.0/255.255.255.0' IDENTIFIED BY 'p'; +CREATE USER ord4@'127.0.0.0/8' IDENTIFIED BY 'p'; +CREATE USER ord5@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'p'; +CREATE USER ord5@'127.0.0.0/24' IDENTIFIED BY 'p'; +connect con1,127.0.0.1,ord1,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +ord1@127.0.0.0/24 +disconnect con1; +connection default; +connect con1,127.0.0.1,ord2,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +ord2@127.0.0.0/16 +disconnect con1; +connection default; +connect con1,127.0.0.1,ord3,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +ord3@127.0.0.1/32 +disconnect con1; +connection default; +connect con1,127.0.0.1,ord4,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +ord4@127.0.0.0/255.255.255.0 +disconnect con1; +connection default; +connect con1,127.0.0.1,ord5,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +ord5@127.0.0.0/24 +disconnect con1; +connection default; +# Non-contiguous masks, IPv6 prefixes and wildcards with '/' are rejected +CREATE USER x@'10.0.0.0/255.0.255.0'; +ERROR HY000: Invalid masked host '10.0.0.0/255.0.255.0' for user 'x' +CREATE USER x@'2001:db8::/32'; +ERROR HY000: Invalid masked host '2001:db8::/32' for user 'x' +CREATE USER x@'10.0.0.%/24'; +ERROR HY000: Invalid masked host '10.0.0.%/24' for user 'x' +# GRANT must not auto-create an account with an invalid mask +GRANT SELECT ON test.* TO g1@'10.0.0.0/33' IDENTIFIED BY 'p'; +ERROR HY000: Invalid masked host '10.0.0.0/33' for user 'g1' +GRANT SELECT ON test.* TO g1@'10.0.0.0/24' IDENTIFIED BY 'p'; +DROP USER g1@'10.0.0.0/24'; +# Hosts without '/' are never validated +CREATE USER w1@'%', w2@'10.0.0.%', w3@'127.0.0._', w4@'localhost', w5@'127.0.0.1', w6@'::1'; +DROP USER w1@'%', w2@'10.0.0.%', w3@'127.0.0._', w4@'localhost', w5@'127.0.0.1', w6@'::1'; +# The mask check is only reached when a row is about to be created, +# so ALTER USER and REVOKE keep their own errors +ALTER USER a1@'10.0.0.0/33' IDENTIFIED BY 'p'; +ERROR HY000: Operation ALTER USER failed for 'a1'@'10.0.0.0/33' +REVOKE SELECT ON test.* FROM a1@'10.0.0.0/33'; +ERROR 42000: There is no such grant defined for user 'a1' on host '10.0.0.0/33' +# RENAME USER validates the target host only +CREATE USER r1@'127.0.0.0/8' IDENTIFIED BY 'p'; +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/16'; +DROP USER r1@'10.0.0.0/16'; +# A pre-existing malformed row still loads, and stays usable for +# REVOKE, RENAME and DROP - acl_load() is deliberately lenient +CREATE USER leg@'10.0.0.0/24' IDENTIFIED BY 'p'; +UPDATE mysql.global_priv SET Host='10.0.0.0/33' WHERE User='leg'; +FLUSH PRIVILEGES; +RENAME USER leg@'10.0.0.0/33' TO leg@'10.0.0.0/16'; +DROP USER leg@'10.0.0.0/16'; +# Host strings are stored verbatim - neither spelling is normalised +SHOW GRANTS FOR c02@'127.0.0.0/24'; +Grants for c02@127.0.0.0/24 +GRANT USAGE ON *.* TO `c02`@`127.0.0.0/24` IDENTIFIED BY PASSWORD '*7B9EBEED26AA52ED10C0F549FA863F13C39E0209' +SHOW CREATE USER n02@'127.0.0.0/255.255.255.0'; +CREATE USER for n02@127.0.0.0/255.255.255.0 +CREATE USER `n02`@`127.0.0.0/255.255.255.0` IDENTIFIED BY PASSWORD '*7B9EBEED26AA52ED10C0F549FA863F13C39E0209' +# Matching and ordering survive a reload through acl_load() +FLUSH PRIVILEGES; +connect con1,127.0.0.1,c02,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +c02@127.0.0.0/24 +disconnect con1; +connection default; +connect con1,127.0.0.1,ord1,p,,$MASTER_MYPORT,; +connection con1; +SELECT CURRENT_USER(); +CURRENT_USER() +ord1@127.0.0.0/24 +disconnect con1; +connection default; +# Cleanup +DROP USER +c01@'127.0.0.0/8', +c02@'127.0.0.0/24', +c03@'127.0.0.1/32', +c04@'127.0.0.0/32', +c06@'0.0.0.0/1', +c18@'127.0.0.0/31', +c19@'127.0.0.0/30', +n01@'127.0.0.0/255.0.0.0', +n02@'127.0.0.0/255.255.255.0', +n03@'127.0.0.1/255.255.255.255', +n04@'127.0.0.0/255.255.255.255', +ord1@'127.0.0.0/8', +ord1@'127.0.0.0/24', +ord2@'127.0.0.0/8', +ord2@'127.0.0.0/16', +ord3@'127.0.0.0/9', +ord3@'127.0.0.1/32', +ord4@'127.0.0.0/255.255.255.0', +ord4@'127.0.0.0/8', +ord5@'127.0.0.0/255.0.0.0', +ord5@'127.0.0.0/24'; diff --git a/mysql-test/main/grant_cidr.test b/mysql-test/main/grant_cidr.test new file mode 100644 index 0000000000000..69747c4b1a64f --- /dev/null +++ b/mysql-test/main/grant_cidr.test @@ -0,0 +1,242 @@ +# +# MDEV-25515 User Account Host Names using CIDR notation +# + +--source include/not_embedded.inc + +--echo # Valid CIDR prefixes +CREATE USER c01@'127.0.0.0/8' IDENTIFIED BY 'p'; +CREATE USER c02@'127.0.0.0/24' IDENTIFIED BY 'p'; +CREATE USER c03@'127.0.0.1/32' IDENTIFIED BY 'p'; +CREATE USER c18@'127.0.0.0/31' IDENTIFIED BY 'p'; +CREATE USER c19@'127.0.0.0/30' IDENTIFIED BY 'p'; +CREATE USER c06@'0.0.0.0/1' IDENTIFIED BY 'p'; + +connect (con1,127.0.0.1,c01,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,c02,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,c03,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,c18,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,c19,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,c06,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +--echo # Malformed masks are rejected at CREATE USER. +--error ER_INVALID_HOST_NETMASK +CREATE USER c08@'127.0.0.0/0' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER c09@'127.0.0.1/0' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER c10@'127.0.0.0/33' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER c11@'127.0.0.0/-1' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER c12@'127.0.0.0/abc' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER c13@'127.0.0.0/' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER c14@'127.0.0.0/24x' IDENTIFIED BY 'p'; + +--echo # CIDR == netmask parity +CREATE USER n01@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'p'; +CREATE USER n02@'127.0.0.0/255.255.255.0' IDENTIFIED BY 'p'; +CREATE USER n03@'127.0.0.1/255.255.255.255' IDENTIFIED BY 'p'; +CREATE USER n04@'127.0.0.0/255.255.255.255' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER n05@'127.0.0.0/0.0.0.0' IDENTIFIED BY 'p'; +CREATE USER c04@'127.0.0.0/32' IDENTIFIED BY 'p'; + +connect (con1,127.0.0.1,n01,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,n02,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,n03,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT +--error ER_ACCESS_DENIED_ERROR +connect (con1,127.0.0.1,n04,p,,$MASTER_MYPORT,); +--replace_result $MASTER_MYSOCK MASTER_SOCKET $MASTER_MYPORT MASTER_PORT +--error ER_ACCESS_DENIED_ERROR +connect (con1,127.0.0.1,c04,p,,$MASTER_MYPORT,); + +--echo # The address must be the network address (no host bits set). +--error ER_INVALID_HOST_NETMASK +CREATE USER c05@'127.0.0.0/1' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER c07@'127.0.0.1/24' IDENTIFIED BY 'p'; +--error ER_INVALID_HOST_NETMASK +CREATE USER n07@'127.0.0.1/255.255.255.0' IDENTIFIED BY 'p'; + + +--echo # host specificity ordering +CREATE USER ord1@'127.0.0.0/8' IDENTIFIED BY 'p'; +CREATE USER ord1@'127.0.0.0/24' IDENTIFIED BY 'p'; + +CREATE USER ord2@'127.0.0.0/8' IDENTIFIED BY 'p'; +CREATE USER ord2@'127.0.0.0/16' IDENTIFIED BY 'p'; + +CREATE USER ord3@'127.0.0.0/9' IDENTIFIED BY 'p'; +CREATE USER ord3@'127.0.0.1/32' IDENTIFIED BY 'p'; + +CREATE USER ord4@'127.0.0.0/255.255.255.0' IDENTIFIED BY 'p'; +CREATE USER ord4@'127.0.0.0/8' IDENTIFIED BY 'p'; + +CREATE USER ord5@'127.0.0.0/255.0.0.0' IDENTIFIED BY 'p'; +CREATE USER ord5@'127.0.0.0/24' IDENTIFIED BY 'p'; + +connect (con1,127.0.0.1,ord1,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,ord2,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,ord3,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,ord4,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,ord5,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +--echo # Non-contiguous masks, IPv6 prefixes and wildcards with '/' are rejected +--error ER_INVALID_HOST_NETMASK +CREATE USER x@'10.0.0.0/255.0.255.0'; +--error ER_INVALID_HOST_NETMASK +CREATE USER x@'2001:db8::/32'; +--error ER_INVALID_HOST_NETMASK +CREATE USER x@'10.0.0.%/24'; + +--echo # GRANT must not auto-create an account with an invalid mask +--error ER_INVALID_HOST_NETMASK +GRANT SELECT ON test.* TO g1@'10.0.0.0/33' IDENTIFIED BY 'p'; +GRANT SELECT ON test.* TO g1@'10.0.0.0/24' IDENTIFIED BY 'p'; +DROP USER g1@'10.0.0.0/24'; + +--echo # Hosts without '/' are never validated +CREATE USER w1@'%', w2@'10.0.0.%', w3@'127.0.0._', w4@'localhost', w5@'127.0.0.1', w6@'::1'; +DROP USER w1@'%', w2@'10.0.0.%', w3@'127.0.0._', w4@'localhost', w5@'127.0.0.1', w6@'::1'; + +--echo # The mask check is only reached when a row is about to be created, +--echo # so ALTER USER and REVOKE keep their own errors +--error ER_CANNOT_USER +ALTER USER a1@'10.0.0.0/33' IDENTIFIED BY 'p'; +--error ER_NONEXISTING_GRANT +REVOKE SELECT ON test.* FROM a1@'10.0.0.0/33'; + +--echo # RENAME USER validates the target host only +CREATE USER r1@'127.0.0.0/8' IDENTIFIED BY 'p'; +--error ER_CANNOT_USER +RENAME USER r1@'127.0.0.0/8' TO r1@'10.1.2.3/24'; +RENAME USER r1@'127.0.0.0/8' TO r1@'10.0.0.0/16'; +DROP USER r1@'10.0.0.0/16'; + +--echo # A pre-existing malformed row still loads, and stays usable for +--echo # REVOKE, RENAME and DROP - acl_load() is deliberately lenient +CREATE USER leg@'10.0.0.0/24' IDENTIFIED BY 'p'; +UPDATE mysql.global_priv SET Host='10.0.0.0/33' WHERE User='leg'; +FLUSH PRIVILEGES; +RENAME USER leg@'10.0.0.0/33' TO leg@'10.0.0.0/16'; +DROP USER leg@'10.0.0.0/16'; + +--echo # Host strings are stored verbatim - neither spelling is normalised +SHOW GRANTS FOR c02@'127.0.0.0/24'; +SHOW CREATE USER n02@'127.0.0.0/255.255.255.0'; + +--echo # Matching and ordering survive a reload through acl_load() +FLUSH PRIVILEGES; +connect (con1,127.0.0.1,c02,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +connect (con1,127.0.0.1,ord1,p,,$MASTER_MYPORT,); +connection con1; +SELECT CURRENT_USER(); +disconnect con1; +connection default; + +--echo # Cleanup + +DROP USER + c01@'127.0.0.0/8', + c02@'127.0.0.0/24', + c03@'127.0.0.1/32', + c04@'127.0.0.0/32', + c06@'0.0.0.0/1', + c18@'127.0.0.0/31', + c19@'127.0.0.0/30', + n01@'127.0.0.0/255.0.0.0', + n02@'127.0.0.0/255.255.255.0', + n03@'127.0.0.1/255.255.255.255', + n04@'127.0.0.0/255.255.255.255', + ord1@'127.0.0.0/8', + ord1@'127.0.0.0/24', + ord2@'127.0.0.0/8', + ord2@'127.0.0.0/16', + ord3@'127.0.0.0/9', + ord3@'127.0.0.1/32', + ord4@'127.0.0.0/255.255.255.0', + ord4@'127.0.0.0/8', + ord5@'127.0.0.0/255.0.0.0', + ord5@'127.0.0.0/24'; + + +# +# End of MDEV-25515 tests +# diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index a9babd0ea928a..62ba074692d06 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -12414,3 +12414,5 @@ ER_PARTITION_INTERVAL_MAXVALUE eng "MAXVALUE is not allowed in range partitioning with interval" ER_RANGE_INTERVAL_PART_FAILED eng "Range partition table %`s.%`s: adding INTERVAL partition(s) failed" +ER_INVALID_HOST_NETMASK + eng "Invalid masked host '%s' for user '%s'" diff --git a/sql/sql_acl.cc b/sql/sql_acl.cc index 1a2af68394868..7b8f69ad69cb9 100644 --- a/sql/sql_acl.cc +++ b/sql/sql_acl.cc @@ -122,6 +122,8 @@ static plugin_ref get_auth_plugin(THD *thd, const LEX_CSTRING &name, bool *locke return my_plugin_lock_by_name(thd, &name, MYSQL_AUTHENTICATION_PLUGIN); } +#define mask_of(h) ((h).ip_mask ? (h).ip_mask : 0xFFFFFFFFL) /* unmasked literal == /32 */ + /* Classes */ struct acl_host_and_ip @@ -3890,6 +3892,14 @@ static int acl_user_compare(const void *a_, const void *b_) if (res) return res; + /* + For masked IP entries, prefer the more specific subnet. Valid subnet + masks with more leading 1-bits have larger numeric values, so sorting + by the mask value orders /32 before /24 before /16, etc. + */ + if (mask_of(a->host) != mask_of(b->host)) + return mask_of(a->host) > mask_of(b->host) ? -1 : 1; + /* For more deterministic results, resolve ambiguity between "localhost" and "127.0.0.1"/"::1" by sorting "localhost" before @@ -5380,8 +5390,10 @@ static ACL_USER_BASE *find_acl_user_base(const LEX_CSTRING &user, hostname (May include wildcards); monty.pp.sci.fi ip (May include wildcards); 192.168.0.0 ip/netmask 192.168.0.0/255.255.255.0 + ip/prefix (CIDR) 192.168.0.0/24 A net mask of 0.0.0.0 is not allowed. + /0 being rejected for the same reason. */ static const char *calc_ip(const char *ip, long *val, char end) @@ -5402,19 +5414,80 @@ static const char *calc_ip(const char *ip, long *val, char end) return ip; } +static const char *calc_cidr(const char *ip, long *val) +{ + long prefix; + /* + CIDR prefix length must be between 1 and 32. + Example - /8 -> 255.0.0.0 + */ + if (!(ip=str2int(ip, 10, 0, 32, &prefix)) || *ip != '\0' || !prefix) + return 0; + + *val= (long) (uint32) (0xFFFFFFFFU << (32 - prefix)); + return ip; +} + +/* + Check that a host given in masked form is well formed: + + a.b.c.d/255.255.255.0 (netmask notation) + a.b.c.d/24 (CIDR notation, RFC 4632) + + Both spellings must yield a contiguous, non-zero mask, and the address + must be the network address (no host bits set), since compare_hostname() + tests (client_ip & ip_mask) == ip and would otherwise never match. + + Only called for hosts containing '/' - plain hostnames, IPs and wildcard + patterns are not masked hosts and are not checked here. +*/ +static bool valid_masked_host(const char *str) +{ + long ip, mask; + const char *p= calc_ip(str, &ip, '/'); + if (!p) + return false; + + if (!calc_ip(p + 1, &mask, '\0') && !calc_cidr(p + 1, &mask)) + return false; + + ulong m= (ulong) (uint32) mask; + if (!m) + return false; + + ulong inv= (~m) & 0xFFFFFFFFUL; + if (inv & (inv + 1)) + return false; + + if (((ulong) (uint32) ip) & inv) + return false; + + return true; +} static void update_hostname(acl_host_and_ip *host, const char *hostname) { // fix historical undocumented convention that empty host is the same as '%' hostname=const_cast(hostname ? hostname : host_not_specified.str); host->hostname=(char*) hostname; // This will not be modified! - if (!(hostname= calc_ip(hostname,&host->ip,'/')) || - !(hostname= calc_ip(hostname+1,&host->ip_mask,'\0'))) + if (!(hostname=calc_ip(hostname, &host->ip, '/'))) { host->ip= host->ip_mask=0; // Not a masked ip + return; } -} + /* + hostname currently points to '/' + Try old style: 10.20.0.0/255.255.0.0 + If that fails: 10.20.0.0/16 + */ + const char *mask_start= hostname + 1; + if (!calc_ip(mask_start, &host->ip_mask, '\0') && + !calc_cidr(mask_start, &host->ip_mask)) + { + host->ip= host->ip_mask= 0; + } +} static bool compare_hostname(const acl_host_and_ip *host, const char *hostname, const char *ip) @@ -5661,6 +5734,11 @@ static int replace_user_table(THD *thd, const User_table &user_table, if (!combo->auth) combo->auth= &auth_no_password; + if (combo->host.str && strchr(combo->host.str, '/') && !valid_masked_host(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); @@ -13325,6 +13403,13 @@ bool mysql_rename_user(THD *thd, List &list) DBUG_ASSERT(!user_from->is_role()); DBUG_ASSERT(!user_to->is_role()); + if (user_to->host.str && strchr(user_to->host.str, '/') && + !valid_masked_host(user_to->host.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.