我在上一篇文章中讲到了如何使用C#模拟用户登录具有验证码网站。今天我就换位思考一下,站在网站开发人员的角度讲一讲验证码的的一个安全问题:及时销毁网站中的验证码。
为了方便大家理解,这里我就以一个投票的应用网站为例进行说明。投票网站首先要防止的就是用户不断点击投票按钮来重复投票;当然,避免重复投票的解决办法有很多,比如记录IP、写入Session、Cookie甚至还有要求用户输入×××号码等。但是你记录IP,那我就写一个程序来模拟发包,每投1票后自动换代理,然后继续投票,如果是写入到Session中那么我写个投票程序,每投1票就重新开启一个新的会话就是。如果是记入Cookie,那我该表Cookie的值再模拟投票发包,要输入×××号进行验证?写个××××××程序也十分容易……
在投票机器人面前,记录IP、记录×××号码、写入Session和Cookie等防作弊技术形同虚设。所以在投票网站中验证码功能必不可少。那么我们将验证码功能加入到网站投票中:
1.生成验证码图片的页面CreateImg.aspx,其后台代码为:
再论验证码安全:请及时销毁你的验证码_安全protected void Page_Load(object sender, EventArgs e)
再论验证码安全:请及时销毁你的验证码_安全     {
再论验证码安全:请及时销毁你的验证码_安全string checkCode = CreateCode(4);//生成随机是4位验证码
再论验证码安全:请及时销毁你的验证码_安全             Session["CheckCode"] = checkCode;//将验证码保存到Session中
再论验证码安全:请及时销毁你的验证码_安全             CreateImage(checkCode);//将验证码以图片的方式输出
再论验证码安全:请及时销毁你的验证码_安全     }    
2.在用户单击投票按钮后触发的事件:
再论验证码安全:请及时销毁你的验证码_安全protected void btnVote_Click(object sender, ImageClickEventArgs e)
再论验证码安全:请及时销毁你的验证码_安全{
再论验证码安全:请及时销毁你的验证码_安全        if (!ValidateUserInfo())//验证IP在数据库中的情况,一个IP一天只能投5张票,从而防止重复投票
再论验证码安全:请及时销毁你的验证码_安全        {
再论验证码安全:请及时销毁你的验证码_安全                UIHelper.Alert(Page, "一个IP一天只能投5张票,请勿重复投票");
再论验证码安全:请及时销毁你的验证码_安全                return;
再论验证码安全:请及时销毁你的验证码_安全        }
再论验证码安全:请及时销毁你的验证码_安全        if (Session["CheckCode"] == null)
再论验证码安全:请及时销毁你的验证码_安全        {
再论验证码安全:请及时销毁你的验证码_安全                UIHelper.Alert(Page, "验证码已过期,请重新输入");
再论验证码安全:请及时销毁你的验证码_安全                return;
再论验证码安全:请及时销毁你的验证码_安全        }
再论验证码安全:请及时销毁你的验证码_安全        if (Session["CheckCode"].ToString().ToLower() != txbCode.Text.ToLower())//验证码忽略大小写
再论验证码安全:请及时销毁你的验证码_安全        {
再论验证码安全:请及时销毁你的验证码_安全                UIHelper.Alert(Page, "验证码错误");
再论验证码安全:请及时销毁你的验证码_安全                return;
再论验证码安全:请及时销毁你的验证码_安全        }
再论验证码安全:请及时销毁你的验证码_安全        else//验证码正确
再论验证码安全:请及时销毁你的验证码_安全        {
再论验证码安全:请及时销毁你的验证码_安全                UpdateVote();//修改数据库,将投票数加1
再论验证码安全:请及时销毁你的验证码_安全        }
再论验证码安全:请及时销毁你的验证码_安全}
OK,大功告成!这个程序逻辑上有问题吗?没有吧,验证码是生成的图片,图片是有干扰因素的,不会被程序识别,而且验证码的内容是保存到服务器的,逻辑处理也是错。似乎一切都那么完美,但是事实并不是这样,对于这样的投票网站,我的投票机器人仍然肆无忌惮的不断切换IP,不断刷票。(要做投票机器人的同志们注意啦,不要看到投票的地方是有验证码的就一筹莫展了哦,也许他的网站就存在以下描述的漏洞哦!)
漏洞就出在投票时对验证码进行验证后没有对服务器上Session中的验证码内容进行销毁。在平时使用IE浏览时,每投票一次后刷新页面,验证码生成页面被重新请求,所以Session值在请求验证码生成页时被替换,所以不会有什么问题。但是现在面对的是投票机器人,我的机器人在第一次请求时获得验证码的图片并展示给用户,用户肉眼识别验证码,然后输入程序的文本框中,由于服务器上验证码的内容并没有被销毁,而且投票程序也不会再请求验证码生成图片的URL,所以接下来每次使用相同的SessionID和用户输入的验证码值,服务器验证投票时 
if (Session["CheckCode"].ToString().ToLower() != txbCode.Text.ToLower())
都会返回false,验证码都是通过的,所以投票自然成功。终究是百密一疏啊!费尽心思防止投票作弊,最终却因为这一个地方的疏忽而前功尽弃,投票作弊成功,投票结果还是被投票机器人所左右。
也许有人想到了,那可以在Session中放置一个标记,如果投票成功了就将标记置“1”,下次请求时判断Session中标记为“1”就拒绝投票就是了。但是投票只是我这里举的一个例子,像论坛这种用验证码防止用户恶意灌水的总不可能限制用户只发一帖吧。论坛发帖时的验证码如果没有被及时销毁,那么我的灌水机器人就仍然可以到处肆意发帖了,哈哈哈哈。
要避免这个漏洞被利用还是很简单,只需要将上面的代码中投票完成后立即将验证码从服务器上销毁即可:
再论验证码安全:请及时销毁你的验证码_安全protected void btnVote_Click(object sender, ImageClickEventArgs e)
再论验证码安全:请及时销毁你的验证码_安全{
再论验证码安全:请及时销毁你的验证码_安全        if (!ValidateUserInfo())//验证IP在数据库中的情况,一个IP一天只能投5张票,从而防止重复投票
再论验证码安全:请及时销毁你的验证码_安全        {
再论验证码安全:请及时销毁你的验证码_安全                UIHelper.Alert(Page, "一个IP一天只能投5张票,请勿重复投票");
再论验证码安全:请及时销毁你的验证码_安全                return;
再论验证码安全:请及时销毁你的验证码_安全        }
再论验证码安全:请及时销毁你的验证码_安全        if (Session["CheckCode"] == null)
再论验证码安全:请及时销毁你的验证码_安全        {
再论验证码安全:请及时销毁你的验证码_安全                UIHelper.Alert(Page, "验证码已过期,请重新输入");
再论验证码安全:请及时销毁你的验证码_安全                return;
再论验证码安全:请及时销毁你的验证码_安全        }
再论验证码安全:请及时销毁你的验证码_安全        if (Session["CheckCode"].ToString().ToLower() != txbCode.Text.ToLower())//验证码忽略大小写
再论验证码安全:请及时销毁你的验证码_安全        {
再论验证码安全:请及时销毁你的验证码_安全                UIHelper.Alert(Page, "验证码错误");
再论验证码安全:请及时销毁你的验证码_安全                return;
再论验证码安全:请及时销毁你的验证码_安全        }
再论验证码安全:请及时销毁你的验证码_安全        else//验证码正确
再论验证码安全:请及时销毁你的验证码_安全        {
再论验证码安全:请及时销毁你的验证码_安全                UpdateVote();//修改数据库,将投票数加1
再论验证码安全:请及时销毁你的验证码_安全        }
再论验证码安全:请及时销毁你的验证码_安全
再论验证码安全:请及时销毁你的验证码_安全Session["CheckCode"] = null;//验证码使用后马上从服务器销毁
再论验证码安全:请及时销毁你的验证码_安全}
这样如果仍然使用相同的SessionID和验证码值,那么将会在 if (Session["CheckCode"] == null)这里判断出验证码已经过期,想成功投票?重新请求验证码页面获得验证码图片,然后重新输入验证码吧!
这个问题虽然看起来不以为然,但是正所谓“千里之堤毁于蚁穴”,只要验证码没有从服务器上销毁,那么页面上的验证码还是形同虚设,和验证码的图片地址为<img src="CreateCheckCode.aspx?code=af5d" alt="验证码">
这样把验证码直接暴露在HTML中或者直接使用文本而不是图片来表示验证码有什么区别呢?
另外有人提到,这里是Session保存验证码才会有这个问题,那完全基于Cookie加密的呢?在前面的文章中我也提到过Cookie加密的方式保存验证码的内容,但是今天我又仔细想了一下,得出结论:验证码内容不能保存到客户端,也就是说根本就不应该使用Cookie加密的方式,Cookie加密保存验证码明文是没有什么意义的,必须要在服务器端保存与验证码相关的信息(比如验证码明文或者验证码加密解密密钥)。为什么不能使用Cookie加密保存验证码?我举个简单的例子吧:
比如现在页面上显示的验证码是1234,同时抓包发现提交的时候Cookie中有值:“EncryptCode=asdf”这是验证码的明文经过加密后的密文,我不知道加密算法是什么,但是我每次程序提交时就将1234作为验证码的值同时将“EncryptCode=asdf”作为Cookie的一部分发送到服务器,那么服务器将1234加密后与发送过来的Cookie值“asdf”一比较,二者相同,验证通过!!!
所以我认为验证码的明文是不可能完全基于客户端的,必须要在服务器上保存与验证码相关的信息(验证码明文或密钥)。既然要在服务器上保存相关信息,那么就可能出现这个漏洞。当然这是我想遍了所有验证码明文保存的方法后得出的结论,我还不敢拍胸脯说这是100%正确的,如果大家认为这个结论不正确,那希望能够提出具体的情况。
希望大家若做过验证码的都再回头看看自己的验证码内容在服务器上及时销毁没有。这个错误很容易犯,我在某大公司的网站上都发现了这个漏洞,可见犯此错的网站绝对不在少数。
最后希望大家的网站更加安全,更加健壮。
【出自博客园深蓝居