提问

问题1:表单提交欺骗攻击是怎么产生的?

问题2:如何从根本上防范表单提交欺骗攻击?

问题3:针对例子,如何编写安全的修改代码?

课程单元

一个简单的论坛

发布文章及其中存在的问题

删除文章及其中存在的问题

总结

1. 一个简单的论坛

1.1. 论坛的数据库结构

一个简单的论坛数据库结构,包括:articleId、userId、title、content、createDate字段

1.2. 简单的论坛列表页面

这是论坛列表,其中,只允许本人修改和删除自己的文章。当前登录用户的userId已经保存在$_SESSION[“userId”]中,只有本人才能看到修改和删除链接。

<?php
$sql = "select * from article";
$result = mysql_query($sql);
while($row = mysql_fetch_array($query)){
    echo "<a href='viewarticle.php?articleId=" . $row["articleId"] . "'>" . $row["title"] . "</a>";
    if($row["userId"] == $_SESSION["userId"]){
        echo "<a href='editarticle.php?articleId=" . $row["articleId"] . "'>修改</a>";
        echo "<a href='delarticle.php?articleId=" . $row["articleId"] . "'>删除</a>";
    }
}

2. 发布文章及其中存在的问题

2.1. 一个简单的发布

下面是发布的html代码。

<script type="text/javascript">
    function checkform(){
        if($("#title") == ""){
            alert("请输入标题!");
            return false;
        }
        return true;
    }
</script>

<form action="savearticle.php" method="post" onsubmit="return checkform();">
    标题:<input type="text" id="title" name="title"><br />
    内容:<textarea id="content" name="content"></textarea><br />
    <input type="submit" value="提交">
</form>

下面是发布的php代码。

<?php
$sql = "insert into article(userId, content, createDate)";
$sql .= " values($_SESSION[userId], '$_POST[title]', '$_POST[content]', current_timestamp)";
mysql_query($sql);

2.2. 发布中存在的问题

用户可以修改发布的客户端代码,绕过JS中对于标题不能为空的判断,直接提交到服务器上。例如,用户可以很方便的通过浏览器的开发者工具,直接删除onsubmit方法,从而绕过JS验证,直接提交服务器保存。

2.3 如何解决发布中的问题

在进行实际的数据库保存之前,必须判断所输入的数据是否都满足要求,才能实际保存进数据库。对于刚才的发布例子,修改如下:

<?php
if(trim($_POST["title"]) == ""){
    die("标题不能为空");
}

$sql = "insert into article(userId, content, createDate)";
$sql .= " values($_SESSION[userId], '$_POST[title]', '$_POST[content]', current_timestamp)";
mysql_query($sql);

3. 删除文章及其中存在的问题

3.1. 一个简单的删除

下面是处理删除的代码。

<?php
$sql = "delete from article where articleId = " . $_GET["articleId"];
mysql_query($sql);

3.2. 删除中存在的问题

用户不一定必须从列表页面请求删除链接,可以自己构造删除链接,将其他文章都删掉。例如,可以通过下面的php代码,将前一万篇文章都删掉。

<?php
for($i = 0; $i < 10000; $i++){
    file_get_contents("http://www.example.com/delarticle.php?articleId=" . $i);
}

3.3. 如何解决删除中的问题

3.3.1. 判断请求来源网站

在进行操作之前,判断来源网站是否是自己网站,能在一定程度上减少攻击。对于刚才的删除例子,修改如下:

<?php
if(!preg_match("/www.example.com/i", $_SERVER["HTTP_REFERER"])){
    die("不允许执行其他来源网站的请求");
}

$sql = "delete from article where articleId = " . $_GET["articleId"];
mysql_query($sql);

3.3.2. 上述方法的缺陷

对于3.3.1的解决方法,同样可以通过伪造$_SERVER[“HTTP_REFERER”]的方式进行攻击。

<?php
$opts = array(
    'http' => array(
        'method' => 'GET',
        'header' => 'Referer: http://www.example.com/articlelist.php'
    )
);

$context = stream_context_create($opts);

for($i = 0; $i < 10000; $i++){
    file_get_contents("http://www.example.com/delarticle.php?articleId=" . $i);
}

3.3.3. 对输入进行合法性检查

上述删除例子中,在删除之前,需要首先确定被删除的文章存在,其次确定该文章确实是执行删除操作的用户发布的文章,之后执行的删除操作才是完全安全的。

<?php
$sql = "select userId from article where articleId = " . $_GET["articleId"];
$result = mysql_query($sql);
if(!$row = mysql_fetch_array($result)){
    die("文章不存在");
}
if($row["userId"] != $_SESSION["userId"]){
    die("只能删除自己的文章");
}

$sql = "delete from article where articleId = " . $_GET["articleId"];
mysql_query($sql);

4. 总结

由于JS脚本是在客户端执行的,用户可以通过各种方式绕过JS脚本,因此,不能依赖于客户端的JS脚本的判断。

由于HTTP头信息可以任意伪造,因此无法判断用户提交的请求是通过浏览器提交的,还是通过其他手工方式提交的。因此表单提交欺骗攻击无法完全杜绝。

根本防范办法在于,对于用户输入的所有内容,都进行合法性检查,检查都通过之后,才进行后续的真正的操作。