mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
MDEV-41099 HANDLER READ missing column privilege check
HANDLER OPEN accepted column-level SELECT grants as table access, but HANDLER READ returns all columns and never checked column privileges. Reopen of a handler table (after FLUSH/ALTER) did no checks at all. Require SELECT on every column (as for SELECT *) when the user has no table-level SELECT, and redo the privilege checks on reopen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Oleksandr "Sanja" Byelkin
parent
95c812d807
commit
ead9739532
@@ -971,4 +971,188 @@ disconnect con1;
|
||||
connection default;
|
||||
drop procedure mysql.p1;
|
||||
drop user u@localhost;
|
||||
#
|
||||
# MDEV-41099 HANDLER READ missing column privilege check
|
||||
#
|
||||
create database db;
|
||||
create table db.t (id int primary key, a varchar(64), b int);
|
||||
insert into db.t values (1,'tok1',100), (2,'tok2',200);
|
||||
create user u@localhost;
|
||||
grant select (id) on db.t to u@localhost;
|
||||
connect con1, localhost, u, , db;
|
||||
handler t open;
|
||||
ERROR 42000: SELECT command denied to user 'u'@'localhost' for table `db`.`t`
|
||||
handler t read first;
|
||||
ERROR 42S02: Unknown table 't' in HANDLER
|
||||
connection default;
|
||||
grant select (a, b) on db.t to u@localhost;
|
||||
connection con1;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
id a b
|
||||
1 tok1 100
|
||||
connect con2, localhost, root;
|
||||
flush tables db.t;
|
||||
connection default;
|
||||
connection con1;
|
||||
select 1 from information_schema.character_sets limit 1;
|
||||
1
|
||||
1
|
||||
connection con2;
|
||||
disconnect con2;
|
||||
connection con1;
|
||||
handler t read next;
|
||||
id a b
|
||||
1 tok1 100
|
||||
# reopen after a column without privilege was added
|
||||
connect con2, localhost, root;
|
||||
alter table db.t add column c int default 42;
|
||||
connection default;
|
||||
connection con1;
|
||||
select 1 from information_schema.character_sets limit 1;
|
||||
1
|
||||
1
|
||||
connection con2;
|
||||
disconnect con2;
|
||||
connection con1;
|
||||
handler t read first;
|
||||
ERROR 42000: SELECT command denied to user 'u'@'localhost' for table `db`.`t`
|
||||
handler t close;
|
||||
# reopen after privileges changed
|
||||
connection default;
|
||||
grant select (c) on db.t to u@localhost;
|
||||
connection con1;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
id a b c
|
||||
1 tok1 100 42
|
||||
connection default;
|
||||
revoke select (id, a, b, c) on db.t from u@localhost;
|
||||
grant select on db.t to u@localhost;
|
||||
connection con1;
|
||||
connect con2, localhost, root;
|
||||
flush tables db.t;
|
||||
connection default;
|
||||
connection con1;
|
||||
select 1 from information_schema.character_sets limit 1;
|
||||
1
|
||||
1
|
||||
connection con2;
|
||||
disconnect con2;
|
||||
connection con1;
|
||||
handler t read next;
|
||||
id a b c
|
||||
1 tok1 100 42
|
||||
connection default;
|
||||
revoke select on db.t from u@localhost;
|
||||
connection con1;
|
||||
connect con2, localhost, root;
|
||||
flush tables db.t;
|
||||
connection default;
|
||||
connection con1;
|
||||
select 1 from information_schema.character_sets limit 1;
|
||||
1
|
||||
1
|
||||
connection con2;
|
||||
disconnect con2;
|
||||
connection con1;
|
||||
handler t read next;
|
||||
ERROR 42000: SELECT command denied to user 'u'@'localhost' for table `db`.`t`
|
||||
connection default;
|
||||
grant select (id, a, b, c) on db.t to u@localhost;
|
||||
connection con1;
|
||||
handler t read next;
|
||||
id a b c
|
||||
1 tok1 100 42
|
||||
handler t close;
|
||||
connection default;
|
||||
revoke select (id, a, b, c) on db.t from u@localhost;
|
||||
connection con1;
|
||||
# column grants through a role
|
||||
connection default;
|
||||
create role r;
|
||||
grant select (id) on db.t to r;
|
||||
grant r to u@localhost;
|
||||
connection con1;
|
||||
set role r;
|
||||
handler t open;
|
||||
ERROR 42000: SELECT command denied to user 'u'@'localhost' for table `db`.`t`
|
||||
connection default;
|
||||
grant select (a, b, c) on db.t to r;
|
||||
connection con1;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
id a b c
|
||||
1 tok1 100 42
|
||||
handler t close;
|
||||
set role none;
|
||||
handler t open;
|
||||
ERROR 42000: SELECT command denied to user 'u'@'localhost' for table `db`.`t`
|
||||
# column grants to PUBLIC
|
||||
connection default;
|
||||
grant select (id) on db.t to public;
|
||||
connection con1;
|
||||
handler t open;
|
||||
ERROR 42000: SELECT command denied to user 'u'@'localhost' for table `db`.`t`
|
||||
connection default;
|
||||
grant select (a, b, c) on db.t to public;
|
||||
connection con1;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
id a b c
|
||||
1 tok1 100 42
|
||||
handler t close;
|
||||
# column grants split between PUBLIC and a role
|
||||
connection default;
|
||||
revoke select (a, b, c) on db.t from public;
|
||||
revoke select (id) on db.t from r;
|
||||
connection con1;
|
||||
handler t open;
|
||||
ERROR 42000: SELECT command denied to user 'u'@'localhost' for table `db`.`t`
|
||||
set role r;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
id a b c
|
||||
1 tok1 100 42
|
||||
handler t close;
|
||||
disconnect con1;
|
||||
connection default;
|
||||
revoke select (id) on db.t from public;
|
||||
delete from mysql.global_priv where user="PUBLIC";
|
||||
flush privileges;
|
||||
drop role r;
|
||||
drop user u@localhost;
|
||||
drop database db;
|
||||
# Invisible columns: INVISIBLE_USER (a) needs a grant,
|
||||
# INVISIBLE_SYSTEM (row_start, row_end) and INVISIBLE_FULL
|
||||
# (DB_ROW_HASH_1) need none, as for SELECT
|
||||
create database db;
|
||||
create table db.t (id int primary key, a int invisible, b blob, unique(b))
|
||||
with system versioning;
|
||||
set time_zone= '+00:00';
|
||||
set timestamp= unix_timestamp('2001-01-01 00:00:00');
|
||||
insert into db.t (id, a, b) values (1, 10, 'x');
|
||||
set timestamp= default;
|
||||
set time_zone= default;
|
||||
create user u@localhost;
|
||||
grant select (id, b) on db.t to u@localhost;
|
||||
connect con1, localhost, u, , db;
|
||||
handler t open;
|
||||
ERROR 42000: SELECT command denied to user 'u'@'localhost' for table `db`.`t`
|
||||
connection default;
|
||||
grant select (a) on db.t to u@localhost;
|
||||
connection con1;
|
||||
set time_zone= '+00:00';
|
||||
select row_start, row_end from t;
|
||||
row_start row_end
|
||||
2001-01-01 00:00:00.000000 2038-01-19 03:14:07.999999
|
||||
handler t open;
|
||||
handler t read first;
|
||||
id a b row_start row_end DB_ROW_HASH_1
|
||||
1 10 x 2001-01-01 00:00:00.000000 2038-01-19 03:14:07.999999 #
|
||||
handler t close;
|
||||
disconnect con1;
|
||||
connection default;
|
||||
drop user u@localhost;
|
||||
drop database db;
|
||||
# End of 10.11 tests
|
||||
|
||||
@@ -1049,5 +1049,145 @@ connection default;
|
||||
drop procedure mysql.p1;
|
||||
drop user u@localhost;
|
||||
|
||||
--echo #
|
||||
--echo # MDEV-41099 HANDLER READ missing column privilege check
|
||||
--echo #
|
||||
create database db;
|
||||
create table db.t (id int primary key, a varchar(64), b int);
|
||||
insert into db.t values (1,'tok1',100), (2,'tok2',200);
|
||||
create user u@localhost;
|
||||
grant select (id) on db.t to u@localhost;
|
||||
connect con1, localhost, u, , db;
|
||||
--error ER_TABLEACCESS_DENIED_ERROR
|
||||
handler t open;
|
||||
--error ER_UNKNOWN_TABLE
|
||||
handler t read first;
|
||||
connection default;
|
||||
grant select (a, b) on db.t to u@localhost;
|
||||
connection con1;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
let $ddl= flush tables db.t;
|
||||
--source grant2_handler_reopen.inc
|
||||
handler t read next;
|
||||
|
||||
--echo # reopen after a column without privilege was added
|
||||
let $ddl= alter table db.t add column c int default 42;
|
||||
--source grant2_handler_reopen.inc
|
||||
--error ER_TABLEACCESS_DENIED_ERROR
|
||||
handler t read first;
|
||||
handler t close;
|
||||
|
||||
--echo # reopen after privileges changed
|
||||
connection default;
|
||||
grant select (c) on db.t to u@localhost;
|
||||
connection con1;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
connection default;
|
||||
revoke select (id, a, b, c) on db.t from u@localhost;
|
||||
grant select on db.t to u@localhost;
|
||||
connection con1;
|
||||
let $ddl= flush tables db.t;
|
||||
--source grant2_handler_reopen.inc
|
||||
handler t read next;
|
||||
connection default;
|
||||
revoke select on db.t from u@localhost;
|
||||
connection con1;
|
||||
let $ddl= flush tables db.t;
|
||||
--source grant2_handler_reopen.inc
|
||||
--error ER_TABLEACCESS_DENIED_ERROR
|
||||
handler t read next;
|
||||
connection default;
|
||||
grant select (id, a, b, c) on db.t to u@localhost;
|
||||
connection con1;
|
||||
handler t read next;
|
||||
handler t close;
|
||||
connection default;
|
||||
revoke select (id, a, b, c) on db.t from u@localhost;
|
||||
connection con1;
|
||||
|
||||
--echo # column grants through a role
|
||||
connection default;
|
||||
create role r;
|
||||
grant select (id) on db.t to r;
|
||||
grant r to u@localhost;
|
||||
connection con1;
|
||||
set role r;
|
||||
--error ER_TABLEACCESS_DENIED_ERROR
|
||||
handler t open;
|
||||
connection default;
|
||||
grant select (a, b, c) on db.t to r;
|
||||
connection con1;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
handler t close;
|
||||
set role none;
|
||||
--error ER_TABLEACCESS_DENIED_ERROR
|
||||
handler t open;
|
||||
|
||||
--echo # column grants to PUBLIC
|
||||
connection default;
|
||||
grant select (id) on db.t to public;
|
||||
connection con1;
|
||||
--error ER_TABLEACCESS_DENIED_ERROR
|
||||
handler t open;
|
||||
connection default;
|
||||
grant select (a, b, c) on db.t to public;
|
||||
connection con1;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
handler t close;
|
||||
|
||||
--echo # column grants split between PUBLIC and a role
|
||||
connection default;
|
||||
revoke select (a, b, c) on db.t from public;
|
||||
revoke select (id) on db.t from r;
|
||||
connection con1;
|
||||
--error ER_TABLEACCESS_DENIED_ERROR
|
||||
handler t open;
|
||||
set role r;
|
||||
handler t open;
|
||||
handler t read first;
|
||||
handler t close;
|
||||
disconnect con1;
|
||||
connection default;
|
||||
revoke select (id) on db.t from public;
|
||||
delete from mysql.global_priv where user="PUBLIC";
|
||||
flush privileges;
|
||||
drop role r;
|
||||
drop user u@localhost;
|
||||
drop database db;
|
||||
|
||||
--echo # Invisible columns: INVISIBLE_USER (a) needs a grant,
|
||||
--echo # INVISIBLE_SYSTEM (row_start, row_end) and INVISIBLE_FULL
|
||||
--echo # (DB_ROW_HASH_1) need none, as for SELECT
|
||||
create database db;
|
||||
create table db.t (id int primary key, a int invisible, b blob, unique(b))
|
||||
with system versioning;
|
||||
set time_zone= '+00:00';
|
||||
set timestamp= unix_timestamp('2001-01-01 00:00:00');
|
||||
insert into db.t (id, a, b) values (1, 10, 'x');
|
||||
set timestamp= default;
|
||||
set time_zone= default;
|
||||
create user u@localhost;
|
||||
grant select (id, b) on db.t to u@localhost;
|
||||
connect con1, localhost, u, , db;
|
||||
--error ER_TABLEACCESS_DENIED_ERROR
|
||||
handler t open;
|
||||
connection default;
|
||||
grant select (a) on db.t to u@localhost;
|
||||
connection con1;
|
||||
set time_zone= '+00:00';
|
||||
select row_start, row_end from t;
|
||||
handler t open;
|
||||
--replace_column 6 #
|
||||
handler t read first;
|
||||
handler t close;
|
||||
disconnect con1;
|
||||
connection default;
|
||||
drop user u@localhost;
|
||||
drop database db;
|
||||
|
||||
--echo # End of 10.11 tests
|
||||
--source include/wait_until_count_sessions.inc
|
||||
|
||||
17
mysql-test/main/grant2_handler_reopen.inc
Normal file
17
mysql-test/main/grant2_handler_reopen.inc
Normal file
@@ -0,0 +1,17 @@
|
||||
# Forces HANDLER tables of connection con1 to be reopened on next read:
|
||||
# $ddl (exclusive MDL) waits for con1 to close the handler table, which
|
||||
# happens when con1 opens any other table.
|
||||
# Expects connection con1 to exist and con2 not to exist.
|
||||
|
||||
connect con2, localhost, root;
|
||||
send_eval $ddl;
|
||||
connection default;
|
||||
let $wait_condition= select count(*) = 1 from information_schema.processlist
|
||||
where state = 'Waiting for table metadata lock' and info = '$ddl';
|
||||
--source include/wait_condition.inc
|
||||
connection con1;
|
||||
select 1 from information_schema.character_sets limit 1;
|
||||
connection con2;
|
||||
reap;
|
||||
disconnect con2;
|
||||
connection con1;
|
||||
@@ -61,6 +61,8 @@
|
||||
#include "sql_base.h" // insert_fields
|
||||
#include "sql_select.h"
|
||||
#include "transaction.h"
|
||||
#include "sql_acl.h" // check_grant_all_columns
|
||||
#include "sql_parse.h" // check_table_access
|
||||
|
||||
#ifdef USE_PRAGMA_IMPLEMENTATION
|
||||
#pragma implementation // gcc: Class implementation
|
||||
@@ -345,8 +347,17 @@ bool mysql_ha_open(THD *thd, TABLE_LIST *tables, SQL_HANDLER *reopen)
|
||||
open_ltable() or open_table() because we would like to be able
|
||||
to open a temporary table.
|
||||
*/
|
||||
error= (thd->open_temporary_tables(tables) ||
|
||||
open_tables(thd, &tables, &counter, 0));
|
||||
error= thd->open_temporary_tables(tables);
|
||||
#ifndef NO_EMBEDDED_ACCESS_CHECKS
|
||||
/*
|
||||
Privileges could have changed since HANDLER OPEN. They are checked
|
||||
again only when the table is reopened (e.g. after FLUSH TABLES or
|
||||
ALTER TABLE); an open handler is not affected by REVOKE.
|
||||
*/
|
||||
if (!error && reopen && !tables->table)
|
||||
error= check_table_access(thd, SELECT_ACL, tables, FALSE, 1, FALSE);
|
||||
#endif
|
||||
error= error || open_tables(thd, &tables, &counter, 0);
|
||||
|
||||
if (unlikely(error))
|
||||
goto err;
|
||||
@@ -361,6 +372,25 @@ bool mysql_ha_open(THD *thd, TABLE_LIST *tables, SQL_HANDLER *reopen)
|
||||
goto err;
|
||||
}
|
||||
|
||||
#ifndef NO_EMBEDDED_ACCESS_CHECKS
|
||||
/*
|
||||
HANDLER READ returns all columns, so with only column-level grants
|
||||
SELECT is required on each of them (as for SELECT *). On reopen the
|
||||
table could have got new columns. Temporary tables need no grants
|
||||
(and are not checked on reopen). INVISIBLE_SYSTEM and INVISIBLE_FULL
|
||||
columns are skipped: they need no column grant for SELECT either
|
||||
(MDEV-14853) and the long unique hash is computed from columns that
|
||||
are checked.
|
||||
*/
|
||||
if (!table->s->tmp_table && !(table->grant.privilege & SELECT_ACL))
|
||||
{
|
||||
Field_iterator_table_ref field_it;
|
||||
field_it.set(tables);
|
||||
if (check_grant_all_columns(thd, SELECT_ACL, &field_it))
|
||||
goto err;
|
||||
}
|
||||
#endif
|
||||
|
||||
DBUG_PRINT("info",("clone_tickets start"));
|
||||
for (TABLE_LIST *table_list= tables; table_list;
|
||||
table_list= table_list->next_global)
|
||||
|
||||
@@ -5808,6 +5808,7 @@ mysql_execute_command(THD *thd, bool is_called_from_prepared_stmt)
|
||||
There is no need to check for table permissions here, because
|
||||
if a user has no permissions to read a table, he won't be
|
||||
able to open it (with SQLCOM_HA_OPEN) in the first place.
|
||||
On implicit reopen mysql_ha_open() checks privileges again.
|
||||
*/
|
||||
unit->set_limit(select_lex);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user