1
0
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:
Oleksandr Byelkin
2026-10-07 14:24:57 +02:00
committed by Oleksandr "Sanja" Byelkin
parent 95c812d807
commit ead9739532
5 changed files with 374 additions and 2 deletions

View File

@@ -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

View File

@@ -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

View 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;

View File

@@ -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)

View File

@@ -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);