• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 数据库安全

    今时今日,数据库系统已经成为各个动态网站上 web 应用程序的重要组成部分。由于非常敏感和机密的数据有可能保存在数据库中,所以对数据库实施保护就显得尤为重要了。

    要从数据库中提取或者存入数据,就必须经过连接数据库、发送一条合法查询、获取结果、关闭连接等步骤。目前,能完成这一系列动作的最常用的查询语言是结构化查询语言 Structured Query Language (SQL)。可以看看攻击者是如何篡改 SQL 查询语句的。

    PHP 本身并不能保护数据库的安全。下面的章节只是讲述怎样用 PHP 脚本对数据库进行基本的访问和操作。

    记住一条简单的原则:深入防御。保护数据库的措施越多,攻击者就越难获得和使用数据库内的信息。正确地设计和应用数据库可以减少被攻击的担忧。

    设计数据库

    第一步一般都是创建数据库,除非是使用第三方的数据库服务。当创建一个数据库的时候,会指定一个所有者来执行和新建语句。通常,只有所有者(或超级用户)才有权对数据库中的对象进行任意操作。如果想让其他用户使用,就必须赋予他们权限。

    应用程序永远不要使用数据库所有者或超级用户帐号来连接数据库,因为这些帐号可以执行任意的操作,比如说修改数据库结构(例如删除一个表)或者清空整个数据库的内容。

    应该为程序的每个方面创建不同的数据库帐号,并赋予对数据库对象的极有限的权限。仅分配给能完成其功能所需的权限,避免同一个用户可以完成另一个用户的事情。这样即使攻击者利用程序漏洞取得了数据库的访问权限,也最多只能做到和该程序一样的影响范围。

    鼓励用户不要把所有的事务逻辑都用 web 应用程序(即用户的脚本)来实现。最好用视图(view)、触发器(trigger)或者规则(rule)在数据库层面完成。当系统升级的时候,需要为数据库开辟新的接口,这时就必须重做所有的数据库客户端。除此之外,触发器还可以透明和自动地处理字段,并在调试程序和跟踪事实时提供有用的信息。

    连接数据库

    把连接建立在 SSL 加密技术上可以增加客户端和服务器端通信的安全性,或者 SSH 也可以用于加密客户端和数据库之间的连接。如果使用了这些技术的话,攻击者要监视服务器的通信或者得到数据库的信息是很困难的。

    加密存储模型

    SSL/SSH 能保护客户端和服务器端交换的数据,但 SSL/SSH 并不能保护数据库中已有的数据。SSL 只是一个加密网络数据流的协议。

    如果攻击者取得了直接访问数据库的许可(绕过 web 服务器),敏感数据就可能暴露或者被滥用,除非数据库自己保护了这些信息。对数据库内的数据加密是减少这类风险的有效途径,但是只有很少的数据库提供这些加密功能。

    对于这个问题,有一个简单的解决办法,就是创建自己的加密机制,然后把它用在 PHP 程序内。PHP 有几个扩展库可以完成这个工作,比如说 Mcrypt 和 Mhash 等,它们包含多种加密运算法则。脚本在插入数据库之前先把数据加密,以后提取出来时再解密。有关加密如何工作的例子请参考相关手册。

    对某些真正隐蔽的数据,如果不需要以明文的形式存在(即不用显示),可以考虑用散列算法。使用散列算法最常见的例子就是把密码经过 MD5 加密后的散列存进数据库来代替原来的明文密码。参见 crypt() 和 md5()。

    <?php
    // 存储密码散列
    $query  = sprintf("INSERT INTO users(name,pwd) VALUES('%s','%s');",
                pg_escape_string($username), md5($password));
    $result = pg_query($connection, $query);
    // 发送请求来验证用户密码
    $query = sprintf("SELECT 1 FROM users WHERE name='%s' AND pwd='%s';",
                pg_escape_string($username), md5($password));
    $result = pg_query($connection, $query);
    if (pg_num_rows($result) > 0) {
        echo 'Welcome, $username!';
    } else {
        echo 'Authentication failed for $username.';
    }
    ?>
    

    SQL 注入

    很多 web 开发者没有注意到 SQL 查询是可以被篡改的,因而把 SQL 查询当作可信任的命令。殊不知道,SQL 查询可以绕开访问控制,从而绕过身份验证和权限检查。更有甚者,有可能通过 SQL 查询去运行主机操作系统级的命令。

    直接 SQL 命令注入就是攻击者常用的一种创建或修改已有 SQL 语句的技术,从而达到取得隐藏数据,或覆盖关键的值,甚至执行数据库主机操作系统命令的目的。这是通过应用程序取得用户输入并与静态参数组合成 SQL 查询来实现的。下面将会给出一些真实的例子。

    由于在缺乏对输入的数据进行验证,并且使用了超级用户或其它有权创建新用户的数据库帐号来连接,攻击者可以在数据库中新建一个超级用户。

    Example #1 一段实现数据分页显示的代码……也可以被用作创建一个超级用户(PostgreSQL系统)。

    <?php
    $offset = $argv[0]; // 注意,没有输入验证!
    $query  = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
    $result = pg_query($conn, $query);
    ?>
    
    一般的用户会点击$offset已被斌值的“上一页”、“下一页”的链接。原本代码只会认为$offset是一个数值。然而,如果有人尝试把以下语句先经过 urlencode() 处理,然后加入URL中的话:
    0;
    insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
        select 'crack', usesysid, 't','t','crack'
        from pg_shadow where usename='postgres';
    --
    那么他就可以创建一个超级用户了。注意那个0;只不过是为了提供一个正确的偏移量以便补充完整原来的查询,使它不要出错而已。

    Note:

    --是 SQL 的注释标记,一般可以使用来它告诉 SQL 解释器忽略后面的语句。

    对显示搜索结果的页面下手是一个能得到密码的可行办法。攻击者所要做的只不过是找出哪些提交上去的变量是用于 SQL 语句并且处理不当的。而这类的变量通常都被用于SELECT查询中的条件语句,如WHERE, ORDER BY, LIMITOFFSET。如果数据库支持UNION构造的话,攻击者还可能会把一个完整的 SQL 查询附加到原来的语句上以便从任意数据表中得到密码。因此,对密码字段加密是很重要的。

    Example #2 显示文章……以及一些密码(任何数据库系统)

    <?php
    $query  = "SELECT id, name, inserted, size FROM products
                      WHERE size = '$size'
                      ORDER BY $order LIMIT $limit, $offset;";
    $result = odbc_exec($conn, $query);
    ?>
    
    可以在原来的查询的基础上添加另一个SELECT查询来获得密码:
    '
    union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
    --
    假如上述语句(使用'--)被加入到$query中的任意一个变量的话,那么就麻烦了。

    SQL 中的 UPDATE 也会受到攻击。这种查询也可能像上面的例子那样被插入或附加上另一个完整的请求。但是攻击者更愿意对SET子句下手,这样他们就可以更改数据表中的一些数据。这种情况下必须要知道数据库的结构才能修改查询成功进行。可以通过表单上的变量名对字段进行猜测,或者进行暴力破解。对于存放用户名和密码的字段,命名的方法并不多。

    Example #3 从重设密码……到获得更多权限(任何数据库系统)

    <?php
    $query = "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
    ?>
    
    但是恶意的用户会把' or uid like'%admin%'; --作为变量的值提交给$uid来改变 admin 的密码,或者把$pwd的值提交为"hehehe', admin='yes', trusted=100 "(后面有个空格)去获得更多的权限。这样做的话,查询语句实际上就变成了:
    <?php
    // $uid == ' or uid like'%admin%'; --
    $query = "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%'; --";
    // $pwd == "hehehe', admin='yes', trusted=100 "
    $query = "UPDATE usertable SET pwd='hehehe', admin='yes', trusted=100 WHERE
    ...;";
    ?>
    

    下面这个可怕的例子将会演示如何在某些数据库上执行系统命令。

    Example #4 攻击数据库所在主机的操作系统(MSSQL Server)

    <?php
    $query  = "SELECT * FROM products WHERE id LIKE '%$prod%'";
    $result = mssql_query($query);
    ?>
    
    如果攻击提交a%' exec master..xp_cmdshell 'net user test testpass /ADD' --作为变量$prod的值,那么$query将会变成
    <?php
    $query  = "SELECT * FROM products
                        WHERE id LIKE '%a%'
                        exec master..xp_cmdshell 'net user test testpass /ADD'--";
    $result = mssql_query($query);
    ?>
    
    MSSQL 服务器会执行这条 SQL 语句,包括它后面那个用于向系统添加用户的命令。如果这个程序是以sa运行而 MSSQLSERVER 服务又有足够的权限的话,攻击者就可以获得一个系统帐号来访问主机了。

    Note:

    虽然以上的例子是针对某一特定的数据库系统的,但是这并不代表不能对其它数据库系统实施类似的攻击。使用不同的方法,各种数据库都有可能遭殃。

    预防措施

    也许有人会自我安慰,说攻击者要知道数据库结构的信息才能实施上面的攻击。没错,确实如此。但没人能保证攻击者一定得不到这些信息,一但他们得到了,数据库有泄露的危险。如果你在用开放源代码的软件包来访问数据库,比如论坛程序,攻击者就很容得到到相关的代码。如果这些代码设计不良的话,风险就更大了。

    这些攻击总是建立在发掘安全意识不强的代码上的。所以,永远不要信任外界输入的数据,特别是来自于客户端的,包括选择框、表单隐藏域和 cookie。就如上面的第一个例子那样,就算是正常的查询也有可能造成灾难。

    • 永远不要使用超级用户或所有者帐号去连接数据库。要用权限被严格限制的帐号。
    • 检查输入的数据是否具有所期望的数据格式。PHP 有很多可以用于检查输入的函数,从简单的变量函数和字符类型函数(比如 is_numeric(),ctype_digit())到复杂的 Perl 兼容正则表达式函数都可以完成这个工作。
    • 如果程序等待输入一个数字,可以考虑使用 is_numeric() 来检查,或者直接使用 settype() 来转换它的类型,也可以用 sprintf() 把它格式化为数字。

      Example #5 一个实现分页更安全的方法

      <?php
      settype($offset, 'integer');
      $query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
      // 请注意格式字符串中的 %d,如果用 %s 就毫无意义了
      $query = sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;",
                       $offset);
      ?>
      
    • 使用数据库特定的敏感字符转义函数(比如 mysql_escape_string() 和 sql_escape_string())把用户提交上来的非数字数据进行转义。如果数据库没有专门的敏感字符转义功能的话 addslashes() 和 str_replace() 可以代替完成这个工作。看看第一个例子,此例显示仅在查询的静态部分加上引号是不够的,查询很容易被攻破。
    • 要不择手段避免显示出任何有关数据库的信心,尤其是数据库结构。参见错误报告和错误处理函数。
    • 也可以选择使用数据库的存储过程和预定义指针等特性来抽象数库访问,使用户不能直接访问数据表和视图。但这个办法又有别的影响。

    除此之外,在允许的情况下,使用代码或数据库系统保存查询日志也是一个好办法。显然,日志并不能防止任何攻击,但利用它可以跟踪到哪个程序曾经被尝试攻击过。日志本身没用,要查阅其中包含的信息才行。毕竟,更多的信息总比没有要好。

    The best way has got to be parameterised queries. Then it doesn't matter what the user types in the data goes to the database as a value. 
    A quick search online shows some possibilities in PHP which is great! Even on this site - http://php.net/manual/en/pdo.prepared-statements.php
    which also gives the reasons this is good both for security and performance.
    Note that PHP 5 introduced filters that you can use for untrusted user input:
    http://us.php.net/manual/en/intro.filter.php

    上篇:文件系统安全

    下篇:错误报告