<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>code.openark.org &#187; Security</title>
	<atom:link href="http://code.openark.org/blog/tag/security/feed" rel="self" type="application/rss+xml" />
	<link>http://code.openark.org/blog</link>
	<description>Blog by Shlomi Noach</description>
	<lastBuildDate>Tue, 07 Sep 2010 05:53:01 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>mylvmbackup HOWTO: minimal privileges &amp; filesystem copy</title>
		<link>http://code.openark.org/blog/mysql/mylvmbackup-howto-minimal-privileges-filesystem-copy</link>
		<comments>http://code.openark.org/blog/mysql/mylvmbackup-howto-minimal-privileges-filesystem-copy#comments</comments>
		<pubDate>Tue, 17 Aug 2010 17:42:40 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Backup]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Open Source]]></category>
		<category><![CDATA[scripts]]></category>
		<category><![CDATA[Security]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=2839</guid>
		<description><![CDATA[This HOWTO discusses two (unrelated) issues with mylvmbackup: The minimal privileges required to take MySQL backups with mylvmbackup. Making (non compressed) file system copy of one&#8217;s data files. Minimal privileges Some just give mylvmbackup the root account, which is far too permissive. We now consider what the minimal requirements of mylvmbackup are. The queries mylvmbackup [...]]]></description>
			<content:encoded><![CDATA[<p>This HOWTO discusses two (unrelated) issues with <a href="http://www.lenzg.net/mylvmbackup/"><em>mylvmbackup</em></a>:</p>
<ul>
<li>The minimal privileges required to take MySQL backups with <em>mylvmbackup.</em></li>
<li>Making (non compressed) file system copy of one&#8217;s data files.</li>
</ul>
<h4>Minimal privileges</h4>
<p>Some just give <em>mylvmbackup</em> the root account, which is far too permissive. We now consider what the minimal requirements of <em>mylvmbackup</em> are.</p>
<p>The queries <em>mylvmbackup</em> issues are:</p>
<ul>
<li><strong>FLUSH TABLES</strong></li>
<li><strong>FLUSH TABLES WITH READ LOCK</strong></li>
<li><strong>SHOW MASTER STATUS</strong></li>
<li><strong>SHOW SLAVE STATUS</strong></li>
<li><strong>UNLOCK TABLES</strong></li>
</ul>
<p>Both <strong>SHOW MASTER STATUS</strong> &amp; <strong>SHOW SLAVE STATUS</strong> require either the <strong>SUPER</strong> or <strong>REPLICATION CLIENT</strong> privilege. Since <strong>SUPER</strong> is more powerful, we choose <strong>REPLICATION CLIENT</strong>.</p>
<p>The <strong>FLUSH TABLES</strong> * and <strong>UNLOCK TABLES</strong> require the <strong>RELOAD</strong> privilege.</p>
<p>However, we are not done yet. <em>mylvmbackup</em> connects to the <strong>mysql</strong> database, which means we must also have some privilege there, too. We choose the <strong>SELECT</strong> privilege.</p>
<p><span id="more-2839"></span>Finally, here are the commands to create a <em>mylvmbackup</em> user with minimal privileges:</p>
<blockquote>
<pre>CREATE USER 'mylvmbackup'@'localhost' IDENTIFIED BY '12345';
GRANT RELOAD, REPLICATION CLIENT ON *.* TO 'mylvmbackup'@'localhost';
GRANT SELECT ON mysql.* TO 'mylvmbackup'@'localhost';
</pre>
</blockquote>
<p>In the <strong>mylvmbackup.conf</strong> file, the correlating rows are:</p>
<blockquote>
<pre>[mysql]
user=mylvmbackup
password=12345
host=localhost
</pre>
</blockquote>
<h4>Filesystem copy</h4>
<p>By default, <em>mylvmbackup</em> creates a <strong>.tar.gz</strong> compressed backup file of your data. This is good if the reason you&#8217;re running <em>mylvmbackup</em> is to, well, make a backup. However, as with all backups, one may be making the backup so as to create a replication server. But in this case you don&#8217;t really want compressed data: you want the data extracted on the replication server, just as it is on the original host.</p>
<p><em>mylvmbackup</em> supports backing up the files using <em>rsync</em>.</p>
<p>To copy MySQL data to a remote host, configure the following in the mylvmbackup.conf file:</p>
<blockquote>
<pre>[fs]
backupdir=shlomi@backuphost:/data/backup/mysql
[misc]
backuptype=rsync
</pre>
</blockquote>
<p>You may be prompted to enter password, unless you have the user&#8217;s public key stored on the remote host.</p>
<p>Normally, <em>rsync</em> is considered as <strong>r</strong>emote-<strong>sync</strong>, but it also works on local file systems. If you have a remote directory mounted on your file system (e.g. with <em>nfs</em>), you can use the fact that <em>rsync</em> works just as well with local file systems:</p>
<blockquote>
<pre>[fs]
backupdir=/mnt/backup/mysql
[misc]
backuptype=rsync
</pre>
</blockquote>
<p>Voila! Your backup is complete.</p>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/mylvmbackup-howto-minimal-privileges-filesystem-copy/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Passwords which are bad for your health</title>
		<link>http://code.openark.org/blog/mysql/passwords-which-are-bad-for-your-health</link>
		<comments>http://code.openark.org/blog/mysql/passwords-which-are-bad-for-your-health#comments</comments>
		<pubDate>Sun, 20 Dec 2009 08:41:20 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Security]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=1770</guid>
		<description><![CDATA[I&#8217;ve seen some passwords to take a few years from my life. I mean, we all know about dictionary words, right? And we&#8217;ve all seen Spaceballs, right? But choosing 12345 as your password is not the only careless option: there are many more! The more I get familiar with user&#8217;s password, the more I see [...]]]></description>
			<content:encoded><![CDATA[<p>I&#8217;ve seen some passwords to take a few years from my life.</p>
<p>I mean, we all know about <a href="http://en.wikipedia.org/wiki/Dictionary_attack">dictionary words</a>, right? And we&#8217;ve all seen <a href="http://www.imdb.com/title/tt0094012/">Spaceballs</a>, right? But choosing <strong>12345</strong> as your password is not the only careless option: there are many more! The more I get familiar with user&#8217;s password, the more I see how so much alike they all are.</p>
<p>Let&#8217;s review some of the commonly used bad password practices:</p>
<ul>
<li>Empty passwords. Need I say more? Apparently yes. So what if <em>&#8220;there&#8217;s only access through firewall from our company&#8217;s IP&#8221;</em>?</li>
<li>Dictionary passwords: real English words like &#8216;<strong>falcon</strong>&#8216; or &#8216;<strong>tiger</strong>&#8216;. Don&#8217;t use these! These are the easiest to attack.</li>
<li>Well known words: how about &#8216;<strong>Gandalf</strong>&#8216;? It&#8217;s not dictionary, but it&#8217;s popular enough to appear in any respectable list. For that matter, look at how well filtered passwords are on RedHat: you can&#8217;t choose a password which is a common first or last name in the US, Italy, or even Israel; which is great!</li>
<li>Common substitues: enough with &#8216;<strong>1nsi9ht</strong>&#8216; and &#8216;<strong>@dm1n</strong>&#8216;! These are almost as easy to break as dictionary words; it&#8217;s just a matter of a few more combinations per word.</li>
<li>Keyboard clustered: say <em>No!</em> to &#8216;<strong>1qa2ws</strong>&#8216;. Don&#8217;t use &#8216;<strong>$rty&amp;*io</strong>&#8216;. They seems to be random at first sight, but look for them on the keyboard: it&#8217;s just your common <em>&#8220;how shall I create a password that&#8217;s so easy to remember I will never forget it?&#8221;</em>. Now <strong>REPLACE(&#8220;remember&#8221;, &#8220;break&#8221;)</strong> and <strong>REPLACE(&#8220;never forget&#8221;, &#8220;always regret&#8221;)</strong>.</li>
<li>Children&#8217;s names, birth dates, <strong>123456</strong>, your car&#8217;s license plate number, your <em>Yahoo!</em> mail password, etc. etc. etc.</li>
</ul>
<p>There are many guidelines for <a href="http://en.wikipedia.org/wiki/Password_strength#Guidelines_for_strong_passwords">choosing strong passwords</a>. And everyone seems to know about it. But I&#8217;m still surprised when I find out the MySQL root password is &#8216;<strong>zxcvbn</strong>&#8216; or &#8216;<strong>pa55wd</strong>&#8216;.</p>
<p>MySQL allows for any character in your password, so you may use punctuations, spaces, and other symbols. This is stronger than plain characters and digits.</p>
<p><span id="more-1770"></span>I think it all boils down to one question: do you really need to <em>remember</em> the password? If so, go ahead and use some personal hints, and make it difficult for the intruder.</p>
<p>If not &#8211; and you can store the passwords, encrypted by stronger passwords on a secure server; on plain paper on your bookshelf; behind your mother&#8217;s cupboard, embedded between her Bridge winnings notes &#8211; then use as strong a password as you can get.</p>
<p>A good tool which I&#8217;ve begun to use recently is <strong>pwgen</strong>. For example:</p>
<blockquote>
<pre>$ pwgen -cn 32 1
na5thoeh4jaeth9OoMooBiosoophuShi
$ pwgen -ycn 32 1
zahC0eehei.tee0pahL3sej2ohv^e8me</pre>
</blockquote>
<p><strong>pwgen</strong> can be instructed to produce or not to produce digits, uppercase letters, special characters, and is very handy.</p>
<h4>Conclusion</h4>
<p>Not all my passwords are so strong; I make the distinction between critical data, confidential data, personal data; the damage done by exposing a password; etc.</p>
<p>If your server is behind firewall, that means you have a reason for not letting people in. Take the next small step and choose strong passwords for your OS, database, htaccess and the rest of the gang.</p>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/passwords-which-are-bad-for-your-health/feed</wfw:commentRss>
		<slash:comments>11</slash:comments>
		</item>
		<item>
		<title>Even more on MySQL password security</title>
		<link>http://code.openark.org/blog/mysql/even-more-on-mysql-password-security</link>
		<comments>http://code.openark.org/blog/mysql/even-more-on-mysql-password-security#comments</comments>
		<pubDate>Mon, 08 Jun 2009 07:28:47 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Security]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=878</guid>
		<description><![CDATA[This post follows Ronald Bradford&#8217;s More Basic MySQL Security, and Lenz Grimmer&#8217;s Basic MySQL Security: Providing passwords on the command line and More on MySQL password security. In Ronald&#8217;s post I&#8217;ve argued that passwords provided on command line are visible in plaintext on &#8220;ps aux&#8221;. Lenz has argued that this is incorrect, providing the source [...]]]></description>
			<content:encoded><![CDATA[<p>This post follows Ronald Bradford&#8217;s <a href="http://ronaldbradford.com/blog/more-basic-mysql-security-2009-05-29/">More Basic MySQL Security</a>, and Lenz Grimmer&#8217;s <a href="http://www.lenzg.net/archives/256-Basic-MySQL-Security-Providing-passwords-on-the-command-line.html">Basic MySQL Security: Providing passwords on the command line</a> and <a href="http://www.lenzg.net/archives/257-More-on-MySQL-password-security.html">More on MySQL password security</a>.</p>
<p>In Ronald&#8217;s post I&#8217;ve argued that passwords provided on command line are visible in plaintext on &#8220;ps aux&#8221;. Lenz has argued that this is incorrect, providing the source code to support that. Giuseppe commenting that this has been fixed since 2002. Later on, Lenz shows that passwords are visible in plaintext on OpenSolaris, Solaris and variants of BSD and SysV.</p>
<p>Mental note: old habits die hard; I must remember to revisit issues from time to time.</p>
<h4>Centralizing</h4>
<p>Back to the question: <em>why use a file to store your password, and not provide it on command line? </em></p>
<p><span id="more-878"></span>As in software programming, where you only define &#8216;magic numbers&#8217; once, as some constant or parameter, thus able to change that value in one single place &#8211; instead of hunting for it throughout the code &#8211; placing the password in a config file helps in changing passwords.</p>
<p>Many utilities can use the config-file syntax: the various mysql clients, mytop, maatkit, others&#8230;</p>
<p>Instead of placing passwords in numerous scripts, crontabs etc., why not write it down in one single place, then let everyone look for it there? (Obviously don&#8217;t give out <strong>root</strong> password to everyone freely, just enough privileges as required).</p>
<p>How frequently does one change passwords? In my experience &#8211; not often. I suspect the reason being the overhead of verifying everyone got the change properly. For command line utilities, a config file may ease the burden.</p>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/even-more-on-mysql-password-security/feed</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Blocking user accounts</title>
		<link>http://code.openark.org/blog/mysql/blocking-user-accounts</link>
		<comments>http://code.openark.org/blog/mysql/blocking-user-accounts#comments</comments>
		<pubDate>Thu, 05 Mar 2009 09:44:40 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[openark kit]]></category>
		<category><![CDATA[Security]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=443</guid>
		<description><![CDATA[A long time missing feature in MySQL is temporarily blocking accounts: denying a user to log in, without affecting any other of her privileges. There is no such privilege as &#8216;LOGIN&#8217; in the grants table, as the ability to log in is the most basic one MySQL allows. This basic privilege is called USAGE. I&#8217;ll [...]]]></description>
			<content:encoded><![CDATA[<p>A long time missing feature in MySQL is temporarily blocking accounts: denying a user to log in, without affecting any other of her privileges.</p>
<p>There is no such privilege as &#8216;LOGIN&#8217; in the grants table, as the ability to log in is the most basic one MySQL allows. This basic privilege is called USAGE.</p>
<p>I&#8217;ll present a hack around this, one which <a title="openark kit: oak-block-account" href="http://code.openark.org/forge/openark-kit/oak-block-account">oak-block-account</a> implements. Before presenting the hack, lets lay down some requirements:</p>
<ul>
<li>A user can be blocked from logging in to MySQL.</li>
<li>Such a blocked user can later be &#8216;released&#8217;, re-enabling him to log in.</li>
<li>It should be possible to determine if a certain user is currently blocked or not.</li>
</ul>
<p><span id="more-443"></span>A first attempt to answer the above requirements is to change the account&#8217;s password. As a naive example, we can set an account&#8217;s password to &#8216;aaaaaaaaa&#8217;. But let&#8217;s consider the following:</p>
<ul>
<li>Will the user be unable to find, by some algorithm or by brute force, the new password?</li>
<li>How can we revert the new password to the original one?</li>
</ul>
<p>Time to look at how MySQL stores passwords, then.</p>
<p>We begin by distinguishing old_passwords (variable <strong>old_passwords</strong>=1) from new passwords.</p>
<ul>
<li>Old passwords are 16 characters long. These are hexadecimal characters.</li>
<li>New passwords are 41 characters long: a leading &#8216;*&#8217; followed by 40 hexadecimal characters.</li>
</ul>
<h4>Blocking with &#8216;new passwords&#8217;</h4>
<p>To disable an account using a &#8216;new password&#8217;, the trick is simply to reverse the password in the <strong>mysql.user</strong> table. So if my password was &#8217;123456&#8242; (strong one, isn&#8217;t it?), then <strong>mysql.user</strong> will have:</p>
<blockquote>
<pre>mysql&gt; select PASSWORD('123456');
+-------------------------------------------+
| PASSWORD('123456')                        |
+-------------------------------------------+
| *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |
+-------------------------------------------+</pre>
</blockquote>
<p>To encode, we do:</p>
<blockquote>
<pre>SET PASSWORD FOR 'some_user'@'some_host' = '9DA2AC2DE76CD7ADD8654EE50192347BE7384BB6*'</pre>
</blockquote>
<p>Let&#8217;s consider the implication of what we just did:</p>
<ul>
<li>The new password is valid, as far as MySQL is concerned. No questions asked.</li>
<li>The user cannot log in with his old password.</li>
<li>Nor can the user log in with <em>any other</em> password, since the <strong>PASSWORD()</strong> function will never return a password ending with &#8216;*&#8217;.</li>
<li>It is easy to see that the user is &#8216;blocked&#8217;: his password ends with &#8216;*&#8217;.</li>
<li>It is easy to restore the original password: we simply reverse the text and call <strong>SET PASSWORD</strong> again.</li>
</ul>
<h4>Blocking with &#8216;old passwords&#8217;</h4>
<p>This part really assumes you&#8217;re using MySQL 4.1 or above. If you&#8217;re one of those &#8216;few&#8217; lucky people, but are unfortunately using old_passwords, here&#8217;s the deal:</p>
<p>Reversing an old password won&#8217;t do, since:</p>
<ul>
<li>The reverse may still consist of an encoding for some password</li>
<li>It&#8217;s impossible to tell if a user is blocked or not.</li>
</ul>
<p>MySQL will only allow 16 or 41 character long passwords (anyway that&#8217;s my finding). So to encode a 16 characters long password, we pad it with 25 (= 41-16) &#8216;~&#8217; characters. Thus, the encoded password &#8216;abcdef0123456789&#8242; turns to &#8216;~~~~~~~~~~~~~~~~~~~~~~~~~abcdef0123456789&#8242;. Again, note the following:</p>
<ul>
<li>The new password is accepted by MySQL as valid.</li>
<li>The user cannot log in with his old password.</li>
<li>Nor can the user log in with <em>any other</em> password, since the <strong>PASSWORD()</strong> function will never return a password starting with &#8216;~&#8217;.</li>
<li>It is easy to see that the user is &#8216;blocked&#8217;: his password starts with 25 &#8216;~&#8217; characters.</li>
<li>It is easy to restore the original password: we simply remove the leading &#8216;~&#8217; characters and call <strong>SET PASSWORD</strong> again.</li>
</ul>
<h4>oak-block-account</h4>
<p>No need to work all this by hand. <a title="openark kit: oak-block-account" href="http://code.openark.org/forge/openark-kit/oak-block-account">oak-block-account</a> is a utility which does exactly that. It can block, release, and even kill accounts. It will automatically detect if the password is &#8216;old&#8217; or &#8216;new&#8217;, or if the account is already blocked or not.</p>
<h4>Other possibilities</h4>
<p>RENAME USER is another trick which could be used: we could take a user&#8217;s login (e.g. &#8216;webuser&#8217;) and rename it to an unknown value, like &#8216;webuser__1q98d4f&#8217;. While it serves the same purpose &#8211; of blocking the user, it has a disadvantage: if by luck or hack the new login is discovered, it could still be used to access the database. The change of password solution ensures there is no user/password combination which will work on the blocked account.</p>
<p>Other options may involve removing the account from <strong>mysql.user</strong>, to put it elsewhere, from where to restore the row when the time comes. I prefer a solution which works on the <strong>mysql</strong> schema itself.</p>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/blocking-user-accounts/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>`;`.`*`.`.` is a valid column name</title>
		<link>http://code.openark.org/blog/mysql/is-a-valid-column-name</link>
		<comments>http://code.openark.org/blog/mysql/is-a-valid-column-name#comments</comments>
		<pubDate>Thu, 12 Feb 2009 04:38:11 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Syntax]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=502</guid>
		<description><![CDATA[And the following query: SELECT `;`.`*`.`.` FROM `;`.`*`; is valid as well. So are the following: DROP DATABASE IF EXISTS `;`; CREATE DATABASE `;`; CREATE TABLE `;`.`*` (`.` INT); CREATE TABLE `;`.```` (`.` INT); CREATE TABLE `;`.`$(ls)` (`.` INT); So, on my Linux machine: root@mymachine:/usr/local/mysql/data# ls -l total 30172 drwx------ 2 mysql mysql 4096 2009-01-11 [...]]]></description>
			<content:encoded><![CDATA[<p>And the following query:</p>
<blockquote>
<pre>SELECT `;`.`*`.`.` FROM `;`.`*`;</pre>
</blockquote>
<p>is valid as well. So are the following:</p>
<blockquote>
<pre>DROP DATABASE IF EXISTS `;`;
CREATE DATABASE `;`;
CREATE TABLE `;`.`*` (`.` INT);
CREATE TABLE `;`.```` (`.` INT);
CREATE TABLE `;`.`$(ls)` (`.` INT);</pre>
</blockquote>
<p><span id="more-502"></span>So, on my Linux machine:</p>
<blockquote>
<pre>root@mymachine:/usr/local/mysql/data# ls -l
total 30172
drwx------ 2 mysql mysql     4096 2009-01-11 08:00 ;
-rw-rw---- 1 mysql mysql 18874368 2009-01-09 19:08 ibdata1
-rw-rw---- 1 mysql mysql  5242880 2009-01-09 19:08 ib_logfile0
-rw-rw---- 1 mysql mysql  5242880 2009-01-09 19:08 ib_logfile1
drwxr-x--- 2 mysql mysql     4096 2008-12-09 11:38 mysql
-rw-rw---- 1 mysql mysql  1423612 2009-01-11 08:00 mysql-bin.000001
-rw-rw---- 1 mysql mysql       19 2009-01-04 09:05 mysql-bin.index
drwx------ 2 mysql mysql     4096 2008-12-21 13:58 sakila
-rw-rw---- 1 mysql root      9783 2009-01-04 09:05 mymachine.err
-rw-rw---- 1 mysql mysql        6 2009-01-04 09:05 mymachine
.pid
drwx------ 2 mysql mysql     4096 2009-01-04 08:30 world</pre>
</blockquote>
<p>Well then&#8230;</p>
<blockquote>
<pre>root@mymachine:/usr/local/mysql/data# <strong>cd ;</strong>
root@mymachine:~#</pre>
</blockquote>
<p>Trying again:</p>
<blockquote>
<pre>root@mymachine:~# <strong>cd -</strong>
/usr/local/mysql/data
root@mymachine:/usr/local/mysql/data# <strong>cd ";"</strong>
root@mymachine:/usr/local/mysql/data/;#</pre>
</blockquote>
<p>And now:</p>
<blockquote>
<pre>root@mymachine:/usr/local/mysql/data/;# <strong>ls -l *.frm</strong>
-rw-rw---- 1 mysql mysql 8554 2009-01-11 08:00 `.frm
-rw-rw---- 1 mysql mysql 8554 2009-01-11 08:00 *.frm
-rw-rw---- 1 mysql mysql 8554 2009-01-11 08:00 $(ls).frm</pre>
</blockquote>
<p>Oh, sorry, I meant:</p>
<blockquote>
<pre>root@mymachine:/usr/local/mysql/data/;# <strong>ls -l "*".frm</strong>
-rw-rw---- 1 mysql mysql 8554 2009-01-11 08:00 *.frm</pre>
</blockquote>
<p>Weird.</p>
<p>As a nice surprise, though, the dot (.) is not allowed in database or table names (but is allowed in column names). Nor are the slash (/) and backslash (\). Look <a title="Schema Object Names" href="http://dev.mysql.com/doc/refman/5.0/en/identifiers.html">here</a> for more on this.</p>
<h4>Support for non English naming</h4>
<p>I kinda new about this all along, but never thought of the consequences. It&#8217;s nice to have a relaxed naming rule (I can even name my tables in Hebrew if I like), but &#8220;nice&#8221; doesn&#8217;t always play along with &#8220;practical&#8221;.</p>
<p>As a Hebrew speaker, I repeatedly encounter issues with using my language. In many applications Hebrew encoding is not supported (many times even UTF8 isn&#8217;t). Not to mention the fact that Hebrew is written from right to left. On many occasion I was irritated by the lack of support for non-English or non-ASCII characters.</p>
<p>But not always and not everywhere. I&#8217;ve had my share of programming languages, and, to be honest, I never expected my programming language to support UTF8 encoding for function names, variables, modules, packages or whatever. Using &#8220;a-zA-Z0-9_&#8221; is <em>just fine</em>. Many people who are not well familiar with English just name their variables in their native language, but written with English characters. This works well till you get someone from outside the country, who doesn&#8217;t speak the language and does not understand (nor can pronounce, nor has the matching keyboard layout or knows how to use it) the names.</p>
<p>In the same way, I have no wish for my table names to be named in Hebrew, German or Japanese names. English is <em>just fine</em>.</p>
<p>Using non-letter characters just adds to the mess. Popular &#8220;command&#8221; characters such as &#8216;~&#8217;, &#8216;,&#8217;, &#8216;:&#8217;, &#8216;;&#8217;, &#8216;*&#8217;, &#8216;?&#8217;, &#8216;(&#8216;, &#8216;$&#8217; are better left alone. They don&#8217;t belong in database or table names (mapped to file names) or column names (internally handled by MySQL).</p>
<p>English has become the <em>de-facto</em> computer world language. Programming languages, file systems, TCP/IP protocols, SQL: everything &#8220;speaks&#8221; English.</p>
<h4>Security</h4>
<p>There&#8217;s another aspect, though: security. It may sound silly, but you can actually write complete scripts in a table&#8217;s name! Not wanting to give the wrong idea, I&#8217;m not presenting some table names which can wreak havoc on your machine if used improperly.</p>
<p>But think about it: don&#8217;t we all use a couple of scripts which backup/clean/automate some stuff for us? Don&#8217;t these scripts just go ahead and read some table names, then do stuff on those tables? How well do they trust table names?</p>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/is-a-valid-column-name/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>MySQL security: data integrity issues</title>
		<link>http://code.openark.org/blog/mysql/mysql-security-data-integrity-issues</link>
		<comments>http://code.openark.org/blog/mysql/mysql-security-data-integrity-issues#comments</comments>
		<pubDate>Wed, 21 Jan 2009 10:32:49 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Configuration]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[sql_mode]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=472</guid>
		<description><![CDATA[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 [...]]]></description>
			<content:encoded><![CDATA[<p>MySQL&#8217;s security model is not as elaborate as other popular databases. It&#8217;s missing quite a lot.</p>
<p>I wish to point out what I think are some very disturbing security holes, which may affect the database integrity.</p>
<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>
<p>I will consider three such issues.</p>
<p><span id="more-472"></span>We will assume a database with two tables, and two users.</p>
<blockquote>
<pre>GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
GRANT SELECT, INSERT, UPDATE, DELETE ON `w2`.* TO 'w2user'@'%';</pre>
</blockquote>
<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>
<blockquote>
<pre>DROP DATABASE IF EXISTS w2;
CREATE DATABASE w2;
USE w2;

DROP TABLE IF EXISTS city;
DROP TABLE IF EXISTS country;

CREATE TABLE country (
  country_id int(11) not null auto_increment,
  name varchar(32) NOT NULL,
  PRIMARY KEY  (country_id)
)ENGINE=INNODB;

CREATE TABLE city (
  city_id int(11) NOT NULL auto_increment,
  name varchar(32) NOT NULL,
  country_id int(11) not null ,
  PRIMARY KEY  (city_id),
  INDEX country_id (country_id),
  FOREIGN KEY (country_id) REFERENCES country(country_id)
                      ON DELETE CASCADE

)ENGINE=INNODB;

INSERT INTO country (country_id, name) values (1, 'gbr');
INSERT INTO country (country_id, name) values (2, 'usa');

INSERT INTO city (name, country_id) values ('london',1);
INSERT INTO city (name, country_id) values ('liverpool',1);
INSERT INTO city (name, country_id) values ('birmingham',1);
INSERT INTO city (name, country_id) values ('ny',2);
INSERT INTO city (name, country_id) values ('boston',2);</pre>
</blockquote>
<p>Both tables are InnoDB, to support transactions and foreign keys.</p>
<p>Obviously, &#8216;root&#8217; is allowed to do anything. But what harm can our unprivileged &#8216;w2user&#8217; do?</p>
<h4>FOREIGN_KEY_CHECKS</h4>
<p>The following <strong>INSERT</strong> should fail:</p>
<blockquote>
<pre>INSERT INTO city (name, country_id) values ('no_city',1234567);</pre>
</blockquote>
<p>But look at the following:</p>
<blockquote>
<pre>mysql&gt; SELECT CURRENT_USER();
+----------------+
| CURRENT_USER() |
+----------------+
| w2user@%       |
+----------------+
1 row in set (0.00 sec)

mysql&gt; SET FOREIGN_KEY_CHECKS=0;
Query OK, 0 rows affected (0.00 sec)

mysql&gt; INSERT INTO city (name, country_id) values ('no_city',1234567);
<span style="color: #993300;">Query OK, 1 row affected (0.01 sec)
</span>
mysql&gt; SET FOREIGN_KEY_CHECKS=1;
Query OK, 0 rows affected (0.00 sec)</pre>
</blockquote>
<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>
<blockquote>
<pre>mysql&gt; SELECT * FROM city;
+---------+------------+------------+
| city_id | name       | country_id |
+---------+------------+------------+
|       1 | london     |          1 |
|       2 | liverpool  |          1 |
|       3 | birmingham |          1 |
|       4 | ny         |          2 |
|       5 | boston     |          2 |
|       6 | no_city    |    <span style="color: #993300;">1234567</span> |
+---------+------------+------------+
6 rows in set (0.01 sec)</pre>
</blockquote>
<p>Yes, it did.</p>
<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>
<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>
<h4>tx_isolation</h4>
<p>When using InnoDB, we can choose one of four isolation levels:</p>
<ul>
<li>READ-UNCOMMITTED</li>
<li>READ COMMITTED</li>
<li>REPEATABLE-READ (default):</li>
<li>SERIALIZABLE</li>
</ul>
<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>
<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>
<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>
<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>
<blockquote>
<pre>mysql&gt; SELECT CURRENT_USER();
+----------------+
| CURRENT_USER() |
+----------------+
| w2user@%       |
+----------------+
1 row in set (0.00 sec)

mysql&gt; SET tx_isolation='READ-UNCOMMITTED';
Query OK, 0 rows affected (0.00 sec)</pre>
</blockquote>
<p>Our &#8216;root&#8217; user does the following:</p>
<blockquote>
<pre>mysql&gt; SELECT CURRENT_USER();
+----------------+
| CURRENT_USER() |
+----------------+
| root@localhost |
+----------------+
1 row in set (0.00 sec)

mysql&gt; START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql&gt; INSERT INTO country (name) VALUES ('nowhere');
Query OK, 1 row affected (0.00 sec)</pre>
</blockquote>
<p>While the transaction is still open, w2user can:</p>
<blockquote>
<pre>mysql&gt; SELECT CURRENT_USER();
+----------------+
| CURRENT_USER() |
+----------------+
| w2user@%       |
+----------------+
1 row in set (0.00 sec)

mysql&gt; SELECT * FROM country;
+------------+---------+
| country_id | name    |
+------------+---------+
|          1 | gbr     |
|          2 | usa     |
|          3 | <span style="color: #993300;">nowhere </span>|
+------------+---------+
3 rows in set (0.00 sec)</pre>
</blockquote>
<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>
<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>
<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>
<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>
<h4>sql_mode</h4>
<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>
<p>In our example, <strong>sql_mode</strong> is set to &#8216;<strong>TRADITONAL</strong>&#8216;, which maps to:</p>
<blockquote>
<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>
</blockquote>
<p>Let&#8217;s add a <strong>TIMESTAMP</strong> column to the country table:</p>
<blockquote>
<pre>ALTER TABLE country ADD COLUMN ts TIMESTAMP NOT NULL;</pre>
</blockquote>
<p>We now try to set a &#8217;0&#8242; value for the time stamps (as user root):</p>
<blockquote>
<pre>mysql&gt; UPDATE country SET ts=NOW();
Query OK, 2 rows affected (0.42 sec)
Rows matched: 2  Changed: 2  Warnings: 0

mysql&gt; UPDATE country SET ts=0;
ERROR 1292 (22007): Incorrect datetime value: '0' for column 'ts' at row 1</pre>
</blockquote>
<p>We got the error becuase of the <strong>NO_ZERO_DATE</strong> part of our <strong>sql_mode</strong>.</p>
<p>But, again, look at what w2user can do:</p>
<blockquote>
<pre>mysql&gt; SELECT @@sql_mode;
+-------------------------------------------------------------------------------------------------------------------------------+
| @@sql_mode                                                                                                                    |
+-------------------------------------------------------------------------------------------------------------------------------+
| STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,TRADITIONAL,NO_AUTO_CREATE_USER |
+-------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql&gt; SET sql_mode='';
Query OK, 0 rows affected (0.00 sec)

mysql&gt; UPDATE country SET ts=0;
<span style="color: #993300;">Query OK, 2 rows affected (0.01 sec)
Rows matched: 2  Changed: 2  Warnings: 0
</span>
mysql&gt; SELECT * FROM country;
+------------+------+---------------------+
| country_id | name | ts                  |
+------------+------+---------------------+
|          1 | gbr  | <span style="color: #993300;">0000-00-00 00:00:00</span> |
|          2 | usa  | <span style="color: #993300;">0000-00-00 00:00:00</span> |
+------------+------+---------------------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>So, are &#8217;0&#8242; values allowed for timestamps in our database or not? Turns out any simple user may decide differently.</p>
<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>
<h4>Conclusion</h4>
<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>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/mysql-security-data-integrity-issues/feed</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Triggers Use Case Compilation, Part I</title>
		<link>http://code.openark.org/blog/mysql/triggers-use-case-compilation-part-i</link>
		<comments>http://code.openark.org/blog/mysql/triggers-use-case-compilation-part-i#comments</comments>
		<pubDate>Mon, 05 Jan 2009 09:55:15 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Triggers]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=313</guid>
		<description><![CDATA[I&#8217;ve run by quite a few triggers lately on production systems. In previous posts, I&#8217;ve written about problems solved with triggers. So here&#8217;s a compilation of some solutions based on triggers; and some problems which are not (yet?) solvable due to current triggers limitations. Triggers can be used to: Maintain integrity Enhance security Enhance logging [...]]]></description>
			<content:encoded><![CDATA[<p>I&#8217;ve run by quite a few triggers lately on production systems. In previous posts, I&#8217;ve written about problems solved with triggers. So here&#8217;s a compilation of some solutions based on triggers; and some problems which are not (yet?) solvable due to current triggers limitations.</p>
<p>Triggers can be used to:</p>
<ul>
<li>Maintain integrity</li>
<li>Enhance security</li>
<li>Enhance logging</li>
<li>Assist with archiving</li>
<li>Restrict table size</li>
<li>Manage caching</li>
<li>Manage counters</li>
</ul>
<p>Triggers are not fast. In fact, they can add quite an overhead if misused. Some of the triggers presented here are known to work on real life production systems, though, and work well. But make sure you benchmark before embarking on extensive application changes.<span id="more-313"></span></p>
<p>I&#8217;ll be using <a title="MySQL's world database setup" href="http://dev.mysql.com/doc/world-setup/en/world-setup.html">MySQL&#8217;s world database</a> in some of the examples.</p>
<h4>Maintaining value integrity</h4>
<p>MySQL can enforce (is you&#8217;re using the right sql_mode), some of the values you set for a column. For example, you should not be allowed to set a TINYINT column value to 500. You may not be allowed to set NULL, or you may have to provide default values.</p>
<p>However, within allowed range, SQL or MySQL in general won&#8217;t help you. Assume you have a &#8220;percent&#8221; column, which holds integer values 0..100. It would be a TINYINT, of course. But setting the value to 103 is perfectly valid in MySQL&#8217;s point of view, though not so in yours.</p>
<p>This is where triggers come in handy. With a trigger, you may truncate illegal values or completely abort the operation if something doesn&#8217;t seem right. For example, we may wish to enforce a city&#8217;s district to be non-empty. We may also wish to ensure that the city&#8217;s population does not exceed its country&#8217;s population:</p>
<blockquote>
<pre>DELIMITER $$
DROP TRIGGER IF EXISTS City_bu $$
CREATE TRIGGER City_bu BEFORE UPDATE ON City
FOR EACH ROW
BEGIN
  DECLARE country_population INT;

  IF (CHAR_LENGTH(NEW.District) = 0) THEN
    SELECT 0 FROM `District must not be empty` INTO @error;
  END IF;

  SELECT MAX(Population) FROM Country
    WHERE Code = NEW.CountryCode INTO country_population;
  IF (NEW.Population &gt; country_population) THEN
    SELECT 0 FROM `City population cannot exceed that of country!` INTO @error;
  END IF;
END $$
DELIMITER ;</pre>
</blockquote>
<p>For example:</p>
<blockquote>
<pre>mysql&gt; UPDATE City SET Population=100000000 WHERE Name='London';
<span style="color: #993300;">ERROR 1146 (42S02): Table 'world.City population cannot exceed that of country!' doesn't exist</span></pre>
</blockquote>
<p>We force the trigger to fail under certain circumstances. Since this is a BEFORE INSERT trigger, failure of the trigger causes aborting the INSERT itself.</p>
<h4>Forcing referential integrity</h4>
<p>If you&#8217;re using MyISAM, Memory or even Maria or Falcon, you don&#8217;t get to use Foreign Keys. MySQL&#8217;s plan is to add foreign keys for all storage engines. The plan is on print for quite a few years now. Till then, you may use triggers to simulate foreign keys, including cascading deletes and updates.</p>
<p>Let&#8217;s consider the tables <strong>City</strong> and <strong>Country</strong>. If we could, we would add the contraint that <strong>City.CountryCode</strong> references <strong>Country.Code</strong>. How can this be achieved with triggers? Here&#8217;s a partial solution, showing a DELETE CASCADE:</p>
<blockquote>
<pre>DELIMITER $$

DROP TRIGGER IF EXISTS City_bi $$
CREATE TRIGGER City_bi BEFORE INSERT ON City
FOR EACH ROW
BEGIN
  IF (NOT EXISTS (SELECT NULL FROM Country WHERE Code=NEW.CountryCode)) THEN
    SELECT 0 FROM `CountryCode does not exist in Country table` INTO @error;
  END IF;
END $$

DROP TRIGGER IF EXISTS Country_ad $$
CREATE TRIGGER Country_ad AFTER DELETE ON Country
FOR EACH ROW
BEGIN
  DELETE FROM City WHERE CountryCode = OLD.Code;
END $$

DELIMITER ;</pre>
</blockquote>
<p>Trying out some queries:</p>
<blockquote>
<pre>mysql&gt; INSERT INTO City (Name, CountryCode) VALUES ('zzimbwawa', 'ZWZ');
<span style="color: #993300;">ERROR 1146 (42S02): Table 'world.CountryCode does not exist in Country table' doesn't exist
</span>
mysql&gt; INSERT INTO City (Name, CountryCode) VALUES ('zzimbwawa', 'GBR');
Query OK, 1 row affected (0.00 sec)

mysql&gt; SELECT COUNT(*) FROM City WHERE CountryCode = 'GBR';
+----------+
| COUNT(*) |
+----------+
|       82 |
+----------+
1 row in set (0.01 sec)

mysql&gt; DELETE FROM Country WHERE Code='GBR';
Query OK, 1 row affected (0.04 sec)

mysql&gt; SELECT COUNT(*) FROM City WHERE CountryCode = 'GBR';
+----------+
| COUNT(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>The above example is partial. It does not handle UPDATEs on both tables. You may also modify it to simulate ON DELETE SET NULL instead of ON DELETE CASCADE.</p>
<h4>Maintaining denormalized data integrity</h4>
<p>Denormalized tables can hold data duplicated in several places. When such data changes in one place, triggers can help out with updating the change in the rest occurrences. A <a href="http://karlssonondatabases.blogspot.com/2008/12/using-triggers-for-performance.html">post</a> was recently written which discusses this issue.</p>
<h4>Archiving</h4>
<p>Assume the following table:</p>
<blockquote>
<pre>DROP TABLE IF EXISTS `logs`;
CREATE TABLE  `logs` (
  `id` int(11) NOT NULL auto_increment,
  `subject` varchar(64) NOT NULL,
  `message` varchar(255) NOT NULL,
  `severity` tinyint(4) NOT NULL default '0',
  PRIMARY KEY  (`id`)
);</pre>
</blockquote>
<p>Logs are something that you want to cleanup regularly, on one hand, but keep at a safe place on the other hand. Let&#8217;s create a <strong>logs_archive</strong> table:</p>
<blockquote>
<pre>CREATE TABLE logs_archive LIKE logs;</pre>
</blockquote>
<p>We can automatically move records from the logs table to the logs_archive table:</p>
<blockquote>
<pre>DELIMITER $$
DROP TRIGGER IF EXISTS logs_bd $$
CREATE TRIGGER logs_bd BEFORE DELETE ON logs
FOR EACH ROW
BEGIN
  INSERT INTO logs_archive SELECT * FROM logs WHERE id=OLd.id;
END $$
DELIMITER ;</pre>
</blockquote>
<p>Example:</p>
<blockquote>
<pre>mysql&gt; INSERT INTO logs (subject, message) VALUES ('info', 'new user created');
Query OK, 1 row affected (0.00 sec)

mysql&gt; INSERT INTO logs (subject, message) VALUES ('info', 'cleanup completed');
Query OK, 1 row affected (0.00 sec)

mysql&gt; SELECT * FROM logs;
+----+---------+-------------------+----------+
| id | subject | message           | severity |
+----+---------+-------------------+----------+
|  1 | info    | new user created  |        0 |
|  2 | info    | cleanup completed |        0 |
+----+---------+-------------------+----------+
2 rows in set (0.00 sec)

mysql&gt; DELETE FROM logs WHERE id = 1;
Query OK, 1 row affected (0.01 sec)

mysql&gt; SELECT * FROM logs;
+----+---------+-------------------+----------+
| id | subject | message           | severity |
+----+---------+-------------------+----------+
|  2 | info    | cleanup completed |        0 |
+----+---------+-------------------+----------+
1 row in set (0.01 sec)

mysql&gt; SELECT * FROM logs_archive;
+----+---------+------------------+----------+
| id | subject | message          | severity |
+----+---------+------------------+----------+
|  1 | info    | new user created |        0 |
+----+---------+------------------+----------+
1 row in set (0.00 sec)</pre>
</blockquote>
<p>We can see that the <strong>logs_archive</strong> table has been filled with rows deleted from <strong>logs</strong> table.</p>
<h4>Logging</h4>
<p>Triggers can be used to automatically log significant events. As an example, let&#8217;s say I have a social network application, in which an &#8216;online_user&#8217; table lists those users which have logged in and have not yet logged out (hence they are assumed to be online):</p>
<blockquote>
<pre>DROP TABLE IF EXISTS `online_user`;
CREATE TABLE `online_user` (
  `online_user_id` int(11) NOT NULL auto_increment,
  `login` VARCHAR(64) CHARSET ascii NOT NULL,
  `ipv4` INT UNSIGNED NOT NULL,
  `ts` TIMESTAMP,
  PRIMARY KEY  (`online_user_id`)
);</pre>
</blockquote>
<p>Our application knows how to handle this table. I can enhance my database with logging by adding a logs table, and additional triggers:</p>
<blockquote>
<pre>DROP TABLE IF EXISTS `logs`;
CREATE TABLE `logs` (
  `logs_id` int(11) NOT NULL auto_increment,
  `ts` TIMESTAMP,
  `message` VARCHAR(255) CHARSET utf8 NOT NULL,
  PRIMARY KEY  (`logs_id`)
);

DELIMITER $$

DROP TRIGGER IF EXISTS online_user_ai $$
CREATE TRIGGER online_user_ai AFTER INSERT ON online_user
FOR EACH ROW
BEGIN
  INSERT INTO logs (message) VALUES (CONCAT('User ',NEW.login, ' has logged in from ', INET_NTOA(NEW.ipv4)));
END $$

DROP TRIGGER IF EXISTS online_user_ad $$
CREATE TRIGGER online_user_ad AFTER DELETE ON online_user
FOR EACH ROW
BEGIN
  INSERT INTO logs (message) VALUES (CONCAT('User ',OLD.login, ' has logged out'));
END $$

DELIMITER ;</pre>
</blockquote>
<p>Let&#8217;s see the effect of managing online users:</p>
<blockquote>
<pre>INSERT INTO online_user (login, ipv4) VALUES ('john', 123456);
INSERT INTO online_user (login, ipv4) VALUES ('mark', 654321);
SELECT SLEEP(12);
DELETE FROM online_user WHERE login = 'john';</pre>
</blockquote>
<p>Checking up on the logs table, we get:</p>
<blockquote>
<pre>mysql&gt; SELECT * FROM `logs`;
+---------+---------------------+------------------------------------------+
| logs_id | ts                  | message                                  |
+---------+---------------------+------------------------------------------+
|       1 | 2008-12-22 11:16:31 | User john has logged in from 0.1.226.64  |
|       2 | 2008-12-22 11:16:31 | User mark has logged in from 0.9.251.241 |
|       3 | 2008-12-22 11:16:43 | User john has logged out                 |
+---------+---------------------+------------------------------------------+
3 rows in set (0.00 sec)</pre>
</blockquote>
<p>The <strong>logs</strong> table can be used for logging any change in any table. The application need not be aware what exactly is being logged.</p>
<p>If the <strong>logs</strong> table uses the MyISAM storage engine, the triggers may want to replace the <strong>INSERT</strong> with an <strong>INSERT DELAYED</strong>, so that they return immediately without waiting for locks on the <strong>logs</strong> table. Assuming no crash occurs right after, a separate thread will collect all inserts on the <strong>logs</strong> table, and handle them in its own free time.</p>
<h4>More to come</h4>
<p>More triggers use case, as well as limitations and workarounds, will be presented in following posts.</p>
<p><a title="Triggers Use Case Compilation, Part II" href="http://code.openark.org/blog/mysql/triggers-use-case-compilation-part-ii">Triggers Use Case Compilation, Part II</a></p>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/triggers-use-case-compilation-part-i/feed</wfw:commentRss>
		<slash:comments>15</slash:comments>
		</item>
		<item>
		<title>Using triggers to block malicious code: an example</title>
		<link>http://code.openark.org/blog/mysql/using-triggers-to-block-malicious-code-an-example</link>
		<comments>http://code.openark.org/blog/mysql/using-triggers-to-block-malicious-code-an-example#comments</comments>
		<pubDate>Thu, 01 Jan 2009 21:05:54 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[Triggers]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=145</guid>
		<description><![CDATA[Web applications face constant exploitation attempts. Those with a user base must keep their users&#8217; private data, well&#8230; private. While the MySQL security model allows restricting users access to databases, tables and even columns, it has no built in feature for restricting the rows access within the given table. One cannot allow a user to [...]]]></description>
			<content:encoded><![CDATA[<p>Web applications face constant exploitation attempts. Those with a user base must keep their users&#8217; private data, well&#8230; private.</p>
<p>While the MySQL security model allows restricting users access to databases, tables and even columns, it has no built in feature for restricting the rows access within the given table.</p>
<p>One cannot allow a user to only update rows 0 through 99, but restrict that user from updating rows 100 to 199. Such restrictions are usually managed in the application level, by adding a necessary &#8220;&#8230; AND filtering_column = some_value&#8230;&#8221;</p>
<p>Many web application have the notion of an &#8216;admin&#8217; account, or several such accounts, which provide greater control over the application. The &#8216;admin&#8217; account is one account to which many attacks are targeted. One such attack is an attempt to modify the admin&#8217;s password, such that the attacker can later log in with and access restricted data.<span id="more-145"></span></p>
<p>Assume the following table:</p>
<blockquote>
<pre><strong>CREATE TABLE </strong>my_users (
  ID <strong>INT NOT NULL AUTO_INCREMENT PRIMARY KEY</strong>,
  username <strong>VARCHAR</strong>(32) <strong>CHARSET </strong>ascii <strong>NOT NULL</strong>,
  password <strong>VARCHAR</strong>(32) <strong>CHARSET </strong>ascii <strong>NOT NULL COLLATE</strong> ascii_bin,
  <strong>UNIQUE KEY</strong>(username)
);</pre>
</blockquote>
<p>Let us also assume we are somewhat careful, so that the passwords are not plaintext, but rather encoded with MD5.</p>
<blockquote>
<pre><strong>INSERT INTO</strong> my_users (username, password) <strong>VALUES</strong>
  ('admin', MD5('qwerty')) ; <span style="color: #008000;">-- Safe password as can be found!</span>
<strong>INSERT INTO</strong> my_users (username, password) <strong>VALUES</strong>
  ('alice', MD5('123456')) ; <span style="color: #008000;">-- Safer yet!</span>

<strong>SELECT </strong>* <strong>FROM </strong>my_users;
+----+----------+----------------------------------+
| ID | username | password                         |
+----+----------+----------------------------------+
|  1 | admin    | d8578edf8458ce06fbc5bb76a58c5ca4 |
|  2 | alice    | e10adc3949ba59abbe56e057f20f883e |
+----+----------+----------------------------------+
2 rows in set (0.00 sec)</pre>
</blockquote>
<p>An attacker will try to set the password for the admin account using security holes in the web application. The web application may execute the following query:</p>
<blockquote>
<pre><strong>UPDATE </strong>my_users <strong>SET </strong>password=MD5('att@cker!') <strong>WHERE </strong>username='admin';</pre>
</blockquote>
<p>The issued query is valid, and should generally be allowed. However, we may decide to block changes to the specific &#8216;admin&#8217; row, in the following manner:</p>
<blockquote>
<pre><code>DELIMITER $$
DROP TRIGGER IF EXISTS my_users_bu $$
CREATE TRIGGER my_users_bu BEFORE UPDATE ON my_users
FOR EACH ROW
BEGIN
  IF (NEW.username='admin') THEN
    SELECT 0 INTO @admin_error FROM `Cannot modify admin data!`;
  END IF;
END $$
DELIMITER ;</code></pre>
</blockquote>
<p>Let&#8217;s try running again the query:</p>
<blockquote>
<pre><strong>UPDATE </strong>my_users <strong>SET </strong>password=MD5('att@cker!') <strong>WHERE </strong>username='admin';

<span style="color: #993300;">ERROR 1146 (42S02): Table 'world.Cannot modify admin data!' doesn't exist</span></pre>
</blockquote>
<p>The query fails, since the <strong>BEFORE UPDATE</strong> trigger fails.<br />
We can tweak the trigger to only allow specific users to modify the row:</p>
<blockquote>
<pre><code>DELIMITER $$
DROP TRIGGER IF EXISTS my_users_bu $$
CREATE TRIGGER my_users_bu BEFORE UPDATE ON my_users
FOR EACH ROW
BEGIN
  IF (NEW.username='admin' AND USER() != 'root@localhost') THEN
    SELECT 0 INTO @admin_error FROM `Cannot modify admin data!`;
  END IF;
END $$
DELIMITER ;</code></pre>
</blockquote>
<p>This way it is possible for the root user to modify the password at will. We can further tweak the trigger to INSERT INTO some log table. The information we may wish to register is USER(), the CURRENT_TIMESTAMP(), old password and new password, and perhaps the CONNECTION_ID(). More data means more means to locate the security breach, and monitoring the log table allows for immediate response for such an attempt.</p>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/using-triggers-to-block-malicious-code-an-example/feed</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Dangers of skip-grant-tables</title>
		<link>http://code.openark.org/blog/mysql/dangers-of-skip-grant-tables</link>
		<comments>http://code.openark.org/blog/mysql/dangers-of-skip-grant-tables#comments</comments>
		<pubDate>Thu, 13 Nov 2008 07:52:41 +0000</pubDate>
		<dc:creator>shlomi</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Configuration]]></category>
		<category><![CDATA[Security]]></category>

		<guid isPermaLink="false">http://code.openark.org/blog/?p=102</guid>
		<description><![CDATA[When MySQL&#8217;s root password is lost and must be reset, there are two popular ways to create a new password. One of the options is far too popular, in my opinion. The preferred way of setting a root&#8217;s password is by using an init-file. The process for doing this is well explained in MySQL&#8217;s manual. [...]]]></description>
			<content:encoded><![CDATA[<p>When MySQL&#8217;s root password is lost and must be reset, there are two popular ways to create a new password. One of the options is far too popular, in my opinion.</p>
<p>The preferred way of setting a root&#8217;s password is by using an init-file. The process for doing this is well explained in <a title="How to Reset the Root Password" href="http://dev.mysql.com/doc/refman/5.0/en/resetting-permissions.html">MySQL&#8217;s manual</a>. Using this method requires creating a simple text file, in which the required</p>
<p><strong><code>GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFY BY '****' WIth GRANT OPTION;</code></strong></p>
<p>(or, alternatively,  <code><strong>SET PASSWORD ...</strong></code>) statement is written.</p>
<p>An entry must be written to my.cnf, or supplied via command line parameters:</p>
<p><strong><code>init-file=/tmp/my-init-file.sql</code></strong></p>
<p>MySQL must then be restarted. Upon restart, and before opening any outside connections, the init-file is executed. Once MySQL is up and running, the init-file entry should be dropped.<span id="more-102"></span></p>
<h4>The bad way</h4>
<p>For some reason, the following method seems to be far more popular: starting MySQL with <strong><code>--skip-grant-tables</code></strong>.</p>
<p>When MySQL is started with this parameter, it completely avoids checking its grant tables upon connection and upon query. This means anyone can log in from anywhere, and do anything on the database.</p>
<p>While the manual does mention this is a less preferred way of doing it, it does not elaborate. Starting MySQL with this parameter is a huge security breach. This is why one may wish to add the <strong><code>--skip-networking</code></strong> parameter, to only allow connection from the localhost (using Unix socket, for example).</p>
<p>Moreover, after MySQL starts, and the necessary <code><strong>GRANT</strong> </code>or <strong><code>CHANGE PASSWORD</code></strong> take place, the server is still unsuitable for connections. This is why it needs to be restarted again, this time without <strong><code>--skip-grant-tables</code></strong>.</p>
<p>So, <strong><code>init-file</code></strong>: one restart; no security issues. <strong><code>skip-grant-tables</code></strong>: two restarts, security breach possible.  We have a winner.</p>
]]></content:encoded>
			<wfw:commentRss>http://code.openark.org/blog/mysql/dangers-of-skip-grant-tables/feed</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
	</channel>
</rss>
