{"id":472,"date":"2009-01-21T12:32:49","date_gmt":"2009-01-21T10:32:49","guid":{"rendered":"http:\/\/code.openark.org\/blog\/?p=472"},"modified":"2009-01-21T12:32:49","modified_gmt":"2009-01-21T10:32:49","slug":"mysql-security-data-integrity-issues","status":"publish","type":"post","link":"https:\/\/code.openark.org\/blog\/mysql\/mysql-security-data-integrity-issues","title":{"rendered":"MySQL security: data integrity issues"},"content":{"rendered":"<p>MySQL&#8217;s security model is not as elaborate as other popular databases. It&#8217;s missing quite a lot.<\/p>\n<p>I wish to point out what I think are some very disturbing security holes, which may affect the database integrity.<\/p>\n<p>This post is not about Roles, Kerberos, IPs and such. It&#8217;s about simple MySQL features, which allow common, unprivileged users, to break data integrity by using unprotected session variables.<\/p>\n<p>I will consider three such issues.<\/p>\n<p><!--more-->We will assume a database with two tables, and two users.<\/p>\n<blockquote>\n<pre>GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;\r\nGRANT SELECT, INSERT, UPDATE, DELETE ON `w2`.* TO 'w2user'@'%';<\/pre>\n<\/blockquote>\n<p>We have one &#8216;root&#8217; user, and one very simple &#8216;w2user&#8217;, which can&#8217;t be accused of having too many privileges. The schema, with some sample data, follows.<\/p>\n<blockquote>\n<pre>DROP DATABASE IF EXISTS w2;\r\nCREATE DATABASE w2;\r\nUSE w2;\r\n\r\nDROP TABLE IF EXISTS city;\r\nDROP TABLE IF EXISTS country;\r\n\r\nCREATE TABLE country (\r\n  country_id int(11) not null auto_increment,\r\n  name varchar(32) NOT NULL,\r\n  PRIMARY KEY  (country_id)\r\n)ENGINE=INNODB;\r\n\r\nCREATE TABLE city (\r\n  city_id int(11) NOT NULL auto_increment,\r\n  name varchar(32) NOT NULL,\r\n  country_id int(11) not null ,\r\n  PRIMARY KEY  (city_id),\r\n  INDEX country_id (country_id),\r\n  FOREIGN KEY (country_id) REFERENCES country(country_id)\r\n                      ON DELETE CASCADE\r\n\r\n)ENGINE=INNODB;\r\n\r\nINSERT INTO country (country_id, name) values (1, 'gbr');\r\nINSERT INTO country (country_id, name) values (2, 'usa');\r\n\r\nINSERT INTO city (name, country_id) values ('london',1);\r\nINSERT INTO city (name, country_id) values ('liverpool',1);\r\nINSERT INTO city (name, country_id) values ('birmingham',1);\r\nINSERT INTO city (name, country_id) values ('ny',2);\r\nINSERT INTO city (name, country_id) values ('boston',2);<\/pre>\n<\/blockquote>\n<p>Both tables are InnoDB, to support transactions and foreign keys.<\/p>\n<p>Obviously, &#8216;root&#8217; is allowed to do anything. But what harm can our unprivileged &#8216;w2user&#8217; do?<\/p>\n<h4>FOREIGN_KEY_CHECKS<\/h4>\n<p>The following <strong>INSERT<\/strong> should fail:<\/p>\n<blockquote>\n<pre>INSERT INTO city (name, country_id) values ('no_city',1234567);<\/pre>\n<\/blockquote>\n<p>But look at the following:<\/p>\n<blockquote>\n<pre>mysql&gt; SELECT CURRENT_USER();\r\n+----------------+\r\n| CURRENT_USER() |\r\n+----------------+\r\n| w2user@%       |\r\n+----------------+\r\n1 row in set (0.00 sec)\r\n\r\nmysql&gt; SET FOREIGN_KEY_CHECKS=0;\r\nQuery OK, 0 rows affected (0.00 sec)\r\n\r\nmysql&gt; INSERT INTO city (name, country_id) values ('no_city',1234567);\r\n<span style=\"color: #993300;\">Query OK, 1 row affected (0.01 sec)\r\n<\/span>\r\nmysql&gt; SET FOREIGN_KEY_CHECKS=1;\r\nQuery OK, 0 rows affected (0.00 sec)<\/pre>\n<\/blockquote>\n<p>What was that? w2user was allowed to temporarily disable foreign key checks, insert an otherwise invalid row, then re-enable checks, and no error was thrown? Wait, did the row really get inserted?<\/p>\n<blockquote>\n<pre>mysql&gt; SELECT * FROM city;\r\n+---------+------------+------------+\r\n| city_id | name       | country_id |\r\n+---------+------------+------------+\r\n|       1 | london     |          1 |\r\n|       2 | liverpool  |          1 |\r\n|       3 | birmingham |          1 |\r\n|       4 | ny         |          2 |\r\n|       5 | boston     |          2 |\r\n|       6 | no_city    |    <span style=\"color: #993300;\">1234567<\/span> |\r\n+---------+------------+------------+\r\n6 rows in set (0.01 sec)<\/pre>\n<\/blockquote>\n<p>Yes, it did.<\/p>\n<p>Disabling FK checks is handy when importing large data from dump, or from CSV, when it is <em>known<\/em> to be valid. For example, when restoring a backup created with mysqldump, FK checks can be safely disabled since dumped data must have been valid. Disabling checks helps in reducing import time.<\/p>\n<p>But I don&#8217;t think normal users should be allowed to set the FOREIGN_KEY_CHECKS variable. This should be restricted to users with the SUPER privilege.<\/p>\n<h4>tx_isolation<\/h4>\n<p>When using InnoDB, we can choose one of four isolation levels:<\/p>\n<ul>\n<li>READ-UNCOMMITTED<\/li>\n<li>READ COMMITTED<\/li>\n<li>REPEATABLE-READ (default):<\/li>\n<li>SERIALIZABLE<\/li>\n<\/ul>\n<p>In <strong>READ-UNCOMMITTED<\/strong>, a transaction can read other open transactions uncommitted data. It&#8217;s usually not a good idea to use this isolation level when working with transactional engines, since it undermines the very foundation of using transactions.<\/p>\n<p>But MySQL, and through it, InnoDB, allow a strange thing: the transaction isolation level can be modified on the run. I consider this to be peculiar and undesired. An isolation level imposes an application logic, which should not be changed. But MySQL also allows different isolation level on a per-connection basis.<\/p>\n<p>Every session can work on a different isolation level. This may be a good idea, when a session wishes to be stricter than the rest of the code, by using the <strong>SERIALIZABLE<\/strong> isolation, for example.<\/p>\n<p>But our w2user may decide to <em>lower<\/em> her session&#8217;s isolation level below the global one. That is, MySQL may be configured to work at <strong>REPEATABLE-READ<\/strong>, but w2user is allowed to:<\/p>\n<blockquote>\n<pre>mysql&gt; SELECT CURRENT_USER();\r\n+----------------+\r\n| CURRENT_USER() |\r\n+----------------+\r\n| w2user@%       |\r\n+----------------+\r\n1 row in set (0.00 sec)\r\n\r\nmysql&gt; SET tx_isolation='READ-UNCOMMITTED';\r\nQuery OK, 0 rows affected (0.00 sec)<\/pre>\n<\/blockquote>\n<p>Our &#8216;root&#8217; user does the following:<\/p>\n<blockquote>\n<pre>mysql&gt; SELECT CURRENT_USER();\r\n+----------------+\r\n| CURRENT_USER() |\r\n+----------------+\r\n| root@localhost |\r\n+----------------+\r\n1 row in set (0.00 sec)\r\n\r\nmysql&gt; START TRANSACTION;\r\nQuery OK, 0 rows affected (0.00 sec)\r\n\r\nmysql&gt; INSERT INTO country (name) VALUES ('nowhere');\r\nQuery OK, 1 row affected (0.00 sec)<\/pre>\n<\/blockquote>\n<p>While the transaction is still open, w2user can:<\/p>\n<blockquote>\n<pre>mysql&gt; SELECT CURRENT_USER();\r\n+----------------+\r\n| CURRENT_USER() |\r\n+----------------+\r\n| w2user@%       |\r\n+----------------+\r\n1 row in set (0.00 sec)\r\n\r\nmysql&gt; SELECT * FROM country;\r\n+------------+---------+\r\n| country_id | name    |\r\n+------------+---------+\r\n|          1 | gbr     |\r\n|          2 | usa     |\r\n|          3 | <span style=\"color: #993300;\">nowhere <\/span>|\r\n+------------+---------+\r\n3 rows in set (0.00 sec)<\/pre>\n<\/blockquote>\n<p>w2user used the <strong>READ-UNCOMMITED<\/strong>, hence was allowed to see the (soon to be rolled back?) &#8216;nowhere&#8217; country. But that country was inserted by a session using the <strong>REPEATABLE-READ<\/strong> level.<\/p>\n<p>Each session confirms to its isolation level rules, and the complaint is not about that. The complaint is with the fact that there&#8217;s a mess in our database.<\/p>\n<p>Working with the <strong>REPEATABLE-READ<\/strong> isolation level should guarantee me some <em>privacy<\/em> in my transaction. My transaction may choose to delete all rows from a table, only to fill them back again, and none (a small white lie here, since locking is also involved) is the wiser. The privacy notion is so inherent, that it&#8217;s shocking to learn that any other connection can knowingly choose to ignore my privacy and see any changes I make. This is why I consider this as a security breach, and not just some isolation nuance.<\/p>\n<p>In my opinion, the isolation level should not be dynamic at all. It must not be changed while the database is running. Perhaps I&#8217;m missing some interesting scenario where it would be desired, but the majority of applications would not find this feature beneficial.<\/p>\n<h4>sql_mode<\/h4>\n<p>I&#8217;ve written about <strong>sql_mode<\/strong> before, and here&#8217;s an example for a data integrity issue caused by weak security:<\/p>\n<p>In our example, <strong>sql_mode<\/strong> is set to &#8216;<strong>TRADITONAL<\/strong>&#8216;, which maps to:<\/p>\n<blockquote>\n<pre>STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,TRADITIONAL,NO_AUTO_CREATE_USER<\/pre>\n<\/blockquote>\n<p>Let&#8217;s add a <strong>TIMESTAMP<\/strong> column to the country table:<\/p>\n<blockquote>\n<pre>ALTER TABLE country ADD COLUMN ts TIMESTAMP NOT NULL;<\/pre>\n<\/blockquote>\n<p>We now try to set a &#8216;0&#8217; value for the time stamps (as user root):<\/p>\n<blockquote>\n<pre>mysql&gt; UPDATE country SET ts=NOW();\r\nQuery OK, 2 rows affected (0.42 sec)\r\nRows matched: 2  Changed: 2  Warnings: 0\r\n\r\nmysql&gt; UPDATE country SET ts=0;\r\nERROR 1292 (22007): Incorrect datetime value: '0' for column 'ts' at row 1<\/pre>\n<\/blockquote>\n<p>We got the error becuase of the <strong>NO_ZERO_DATE<\/strong> part of our <strong>sql_mode<\/strong>.<\/p>\n<p>But, again, look at what w2user can do:<\/p>\n<blockquote>\n<pre>mysql&gt; SELECT @@sql_mode;\r\n+-------------------------------------------------------------------------------------------------------------------------------+\r\n| @@sql_mode                                                                                                                    |\r\n+-------------------------------------------------------------------------------------------------------------------------------+\r\n| STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,TRADITIONAL,NO_AUTO_CREATE_USER |\r\n+-------------------------------------------------------------------------------------------------------------------------------+\r\n1 row in set (0.00 sec)\r\n\r\nmysql&gt; SET sql_mode='';\r\nQuery OK, 0 rows affected (0.00 sec)\r\n\r\nmysql&gt; UPDATE country SET ts=0;\r\n<span style=\"color: #993300;\">Query OK, 2 rows affected (0.01 sec)\r\nRows matched: 2  Changed: 2  Warnings: 0\r\n<\/span>\r\nmysql&gt; SELECT * FROM country;\r\n+------------+------+---------------------+\r\n| country_id | name | ts                  |\r\n+------------+------+---------------------+\r\n|          1 | gbr  | <span style=\"color: #993300;\">0000-00-00 00:00:00<\/span> |\r\n|          2 | usa  | <span style=\"color: #993300;\">0000-00-00 00:00:00<\/span> |\r\n+------------+------+---------------------+\r\n2 rows in set (0.00 sec)<\/pre>\n<\/blockquote>\n<p>So, are &#8216;0&#8217; values allowed for timestamps in our database or not? Turns out any simple user may decide differently.<\/p>\n<p>See my earlier posts <a title=\"Do we need sql_mode?\" href=\"http:\/\/code.openark.org\/blog\/mysql\/do-we-need-sql_mode\">here<\/a> and <a title=\"sql_mode: a suggestion\" href=\"http:\/\/code.openark.org\/blog\/mysql\/sql_mode-a-suggestion\">here<\/a>. <a title=\"Roland Bouman's Blog\" href=\"http:\/\/rpbouman.blogspot.com\/\">Roland Bouman<\/a> also offers <a title=\"MySQL's sql_mode: My Suggestions\" href=\"http:\/\/rpbouman.blogspot.com\/2009\/01\/mysqls-sqlmode-my-suggestions.html\">suggestions<\/a> for fixing this issue.<\/p>\n<h4>Conclusion<\/h4>\n<p>The above three examples show how simple users can break data integrity due to very permissive MySQL logic. Even when the database is carfully tuned and secured, there&#8217;s no way to prevent non privileged users from damaging its integrity.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>MySQL&#8217;s security model is not as elaborate as other popular databases. It&#8217;s missing quite a lot. I wish to point out what I think are some very disturbing security holes, which may affect the database integrity. This post is not about Roles, Kerberos, IPs and such. It&#8217;s about simple MySQL features, which allow common, unprivileged [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"enabled":false},"version":2}},"categories":[5],"tags":[11,16,35],"class_list":["post-472","post","type-post","status-publish","format-standard","hentry","category-mysql","tag-configuration","tag-security","tag-sql_mode"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p2bZZp-7C","_links":{"self":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/472","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/comments?post=472"}],"version-history":[{"count":22,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/472\/revisions"}],"predecessor-version":[{"id":530,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/posts\/472\/revisions\/530"}],"wp:attachment":[{"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/media?parent=472"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/categories?post=472"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/code.openark.org\/blog\/wp-json\/wp\/v2\/tags?post=472"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}