基础PHP开发安全Part 1: addslahes/daddslashes addslahes是PHP自带的一个函数,其转换规则为 \ => \\ ' => \' " => \" NUL => \0 其中关键在 ' 和 " 上,这两个过滤非常重要,从某种角度说,这个过滤已经回避了很多SQL注入和PHP入侵。 在最新的Discuz!X系列中,$_GET等都未经过addslashes,如果在程序中直接用DB::query执行SQL查询,并且没有使用%s,那么您需要进行必要的检测。 example 1: Intval <?php ... $num=intval($_GET[‘num’]); DB::query(“UPDATE %t SET `text`=’{$num}’ WHERE uid=’{$_G[‘uid’]’}”, array(‘pluginx_test’)); … ?> example 2: String <?php … $str=addslashes($_GET[‘str’]); DB::query(“UPDATE %t SET `text`=’{$str}’ WHERE uid=’{$_G[‘uid’]}’”, array(‘pluginx_test’)); … ?> example 3: Intval via %d <?php … DB::query(“UPDATE %t SET `text`=%d WHERE uid=%d”, array(‘pluginx_test’, $_GET[‘num’], $_G[‘uid’])); … ?> example 4: String via %s <?php … DB::query(“UPDATE %t SET `text`=%s WHERE uid=%d”, array(‘pluginx_test’, $_GET[‘str’], $_G[‘uid’])); … ?> 但通过数据库/数据表类的入库方法(::insert, ::update)入库时,这些函数会进行必要的处理,所以,请不要进行重复处理。 example 5: ::insert <?php … $str=$_GET[‘str’]; $str=addslashes($_GET[‘str’]); DB::insert(‘pluginx_test’, array(‘uid’=>$_G[‘uid’], ‘text’=>$_GET[‘str’])); … ?> example 6: ::insert <?php … DB::insert(‘pluginx_test’, array(‘uid’=>$_G[‘uid’], ‘text’=>$_GET[‘str’])); DB::insert(‘pluginx_test’, array(‘uid’=>$_G[‘uid’], ‘text’=>addslashes($_GET[‘str’]))); … ?> 同时补充,如果您的插件中使用了iconv等编码转换函数,特别是从UTF-8转成GBK这类情况,并且您需要addslashes,请记得先转换编码,再进行addslashes过滤。因为在UTF-8中的/, \, ‘, “”等敏感符有七八种表示方式,大多不会被addslashes处理,只有先转换编码后才能开始安全处理。此外,urldecode等也在此列。 Part 2: 从引号开始一般来说,ASP常常比PHP容易入侵,很大程度是因为很多ASP初学者,对数据库的安全过滤一概不知,我们常常可以看到下面的这种语句(假设members有三列, id username password,实际需要时可以穷举列数尝试攻击)。 example 7: Without a quote <?php … $query=DB::query(“SELECT username FROM members where id=$id”); … ?>
如果$id可以为人为构造的string,则可能有以下语句: 令 string $id = 9999999999 union (all/distinct) select password FROM members where id = 1 SELECT username FROM members where id = 9999999999 union select password as username FROM members where id = 1 理论上可以获得id=1(常常是管理员)的密码,虽然打开 Discuz!的SQL安全机制后,以上攻击失效。 于是,非常重要的,你必须加上一个引号,并且保证 $id 是int类型,或者已经被有效的addslashes,包括数字类型。 example 8: With a quote <?php … $query=DB::query(“SELECT username FROM members where id=’$id’”); … ?> 目前所知,并没有有效的办法攻破上述安全处理。 国内安全组织80vul特别提醒,in()/limit/order by/group by 这四个地方都是容易忘记加引号的,在本审核员两个月中的审核中,有一款插件因为in()轻信了dimplode的addslashes处理,忽视了此处仍容易被注入,而被打回。 timestamp time() date() Part 3. 引用PHP代码文件首先我们引入一段令人非常无语的代码作为例子。 example 10: <?php require $_GET['path'].'.php'; ?> 简直太容易了,如果可以打开URL,我就可以执行远程脚本了。 http%3A%2F%2Fwww.example.com%2Fx.txt%00 http%3A%2F%2Fwww.example.com%2Fx.txt? http%3A%2F%2Fwww.example.com%2Fx.txt# %00(NUL) ? # 都可以做URL的截断符,使得后面的.php不影响。 在此特别重要的警示,绝对不要写出这样的代码,否则可能会发生非常多的情况,在此建议几种可行的方法 example 11: <?php if(!in_array($_GET['path'], array('zip','bond','download'))) exit; require $_GET['path'].'.php'; ?> example 12: <?php … if(!in_array($_GET['mod'], array('index', 'add'))) $_GET['mod']='index'; … ?> 另外,一些插件需要引用一些PHP文件,这些PHP文件的名字并不确定,例如7ree的某漫画插件中,需要引用某名字的PHP文件,这个文件记载了漫画的信息,而这个名字由客户端提供,不能保证安全性。几次打回,分别是由于没有处理../和%00,最后要求使用白名单的方法来解决,即只容许某个范围的字符输出。preg_match的\w一般可以满足要求,建议大家参考。 Part 4. GPC过滤大概的情况就是,对于一个外部变量,你必须有准确的限制。该是数字的,保证它是数字。该是正常文字的,保证它是正常文字。这里很大程度上是为了防止数据库注入及XSS。 intval 整数特别注意,用intval处理一个非数字的字符串,仍然可以得出一个数字的结果, 比如intval("abcdefg+123456abcdefg-1111"); 结果为123456。 因为intval的原理是:从左边第一个数字(或正负号)开始,一直读到数字结束。 还有特别要注意的,intval不会解决负数的问题,建议再进行 > 0 的检查 is_numeric 数字请特别注意,如果你只允许正数的话,就不要使用这个函数,因为他可能产生好长好长的一个数字串,好像造成了PHPWind的一次运行漏洞。 addslashes 可能要进库的字符串addslashes是非常重要的一个过滤,尤其要注意,不要重复过滤。 strip_tags 对于不应该有HTML的文本这主要是 XSS 的要求,避免被 XSS ,重点就是避免用户的HTML标签直接输出到浏览器上。 htmlspecialchars 对于可能有HTML的文本,但是不希望这些代码被解析。同strip_tags(),有区别的是,他不会去掉标签,而是变成可阅读的样式。 MySQL的安全问题Part 5. 涉及搜索的插件,使用LIKE匹配时的注意许多涉及搜索的程序都会忘记下面这两个字符的过滤: % _ % 相当于 .* _ 相当于 .+ Discuz! 的过滤代码: addcslashes($keyword, '%_') Part 6. 加减法问题,防止积分暴涨对于一个unsigned int,如果你把0再减去1会怎样呢?是的,他会变成四十二亿多(1 << 32 -1) UPDATE test set num=num-1 (此时ccc=0) num=4294967295 该漏洞曾经发生在 PHPWind 的天使宠物插件上,用户现金为零时,打怪遇到惩罚事件掉金钱,结果直接获得高额的奖励。 UPDATE test set ccc=ccc+(-1) (此时ccc=0) num=4294967295 再次变成四十二亿多 对可能为负数的数字,加一个引号,可以解决这个问题: UPDATE test set ccc=ccc+'-1' (此时ccc=0) ccc=0 没有发生变化 著名插件作者 虾米:对于mysql建表的时候,数字不代表你使用的数字最大的位数,例如 timestamp int(10) unsigned,这一个大家应该非常熟悉了,这一个括号中的10并不代表timestamp这个数字最多只能十位(具体的意思自己查手册吧,不知道也无妨),如果不知道是否应该用unsigned的时候就不要用unsigned的,涉及到积分的字段全部不要使用unsigned的(安全起见,宁可让用户负分也不能是正的无穷大) Part 7. 复杂的MySQL问题(来源于80vul)两个MySQL截断问题,第一个是超长截断,第二个是加一个0xc1截断(UTF8)。 我们用例子来说 example 9:MySQL Problem #1 MySQL: CREATE TABLE IF NOT EXISTS `pre_test` ( `uid` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(13) NOT NULL, `password` char(32) NOT NULL, PRIMARY KEY (`uid`), UNIQUE KEY `username` (`username`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=2; INSERT INTO `pre_test` (`uid`, `username`, `password`) VALUES (1, 'admin', 'aeb0908baa5b97e574c5b1825d8f4b92'); <?php … if(!DB::fetch_first(“SELECT * FROM pre_test WHERE username=%s”, array($_GET[‘username’]))){ DB::query(“REPLACE INTO pre_test set username=%s, password=%s”, array($_GET[‘username’], md5($_GET[‘password’]))); } … ?> 我现在用username=” admin lalala” password=’123456’来注册,那么fetch_first处执行的查询是 SELECT * FROM pre_test WHERE username='admin lalala' 查询结果: MySQL 返回的查询结果为空 (即零行)。 零行的原因:因为数据库中的username不可能那么长!肯定找不到对应的! 下面DB::query得以执行, REPLACE INTO pre_test set username='admin lalala', password='e10adc3949ba59abbe56e057f20f883e' 查询结果:影响了 2 行。插入的行 id: 2 ( 查询花费 0.0003 秒 ) 当长长的username进入数据库时,超长部分自动被截去,空格又被忽略,使得username与admin相同。 当然这种方法并非完美,admin的uid已经发生了变化,这意味着创始人身份无法延续,版主的身份也会丢失,但该用户可以是管理员,至少已经拥有了管理员的名字。 其实,如果你不用REPLACE INTO,这个问题就可以小很多,这样顶多造成MySQL Erro因为一般情况下username是UNIQUE索引的,INSERT INTO不会正常执行。 PHPWind就曾经在这件事上摔跤过,当时是用第二个方法,也就是我们即将介绍的,0xc1截断修改任意用户密码(0xC1不是四个字,而是一个字符)。在UTF-8中,0xC1会使MySQL截断后面的数据。当我们请求的用户名“甜橙”后面加上0xC1,那么包括这个字符,及其之后的,在插入时都会被忽略。我们来模拟下。 SELECT * from test where username='甜橙' 查询结果: 甜橙 864411 SELECT * from test where username=concat('甜橙', 0xc1) 查询结果:MySQL 返回的查询结果为空 (即零行)。 REPLACE INTO test set username=concat('甜橙', 0xc1), password='123456' (模拟PHPWind早期的注册) 甜橙密码被修改: 甜橙 123456 简单说,执行一个查询,可能结果为空,但是一会插入信息时,却实际覆盖了原有的数据。使用REPLACE INTO时,开发者要特别小心。 高级PHP开发安全Part 8. 字符串可能是数组非常少见的一个问题,但提出来吸引眼球。 example 13: <?php $s='gnaojgapjhpajgpa'; ?> 此时 $s[1]=='n' example 14: <?php $s=array('nbaohpas', 'gkhaopjgpaj'); ?> 此时 $s[1]=='gkhaopjgpaj' 如果你要获得用户请求的数据的某一个字节,并且你考虑用[1]这样的方法来取,而不是substr,那么一定要用户给的数据是不是一个字符串,否则,你可能「拿」到一个字符串、一个数组等等…… 解决方法:检查是不是字符串,或者干脆就变成字符串好了。也可以用{1}来取。 example 15: <?php $s=array('nbaohpas', 'gkhaopjgpaj'); $s=(string)$s; ?> 此时 $s{1}=='r',因为$s=='Array' 也可以直接拒绝这类非法的信息 example 16: <?php if(!is_string($s)) exit('xxxxx'); ?> Part 9. 随机数的可靠性由于rand函数可能只有三万多种变化,一般不适合用于有认证性的场合,首先,我们建议无论如何使用mt_rand代替rand. 不要用随机函数去创造你认为安全的随机数,一个简单的原因,随机数的产生是根据种子的,种子相同时,从生成种子到第n次求随机数的结果都是完全相同的。考虑到PHP的多个版本存在随机数问题,Discuz!某版本后,决定采用md5散列函数来进行随机,random()函数回避了许多问题,建议替换: string random(int $length, bool $numeric = 0) 在产生安全认证级别的密串时,这可能是一个大问题。某版本的Discuz!论坛的找回密码功能曾被破解,用于重设任意用户的密码。 如果你要获得用户请求的数据的某一个字节,并且你考虑用[1]这样的方法来取,而不是 Part 10. CSRFCSRF有两个特点:普遍、有害。绝大多数的插件,刚注意到CSRF的情况时,往往有许多作品需要大范围地改写,而且它具有有害性,即凡是CSRF攻击,往往都大大小小有利用价值,但是往往不会造成数据库一类的严重安全问题。 首先,我们先讲CSRF在普通用户范围的存在意义,我们先举一个例子,假设有一个版主投票插件,每个人点击“支持“按钮,该版主获得一票,也可以投“反对”票。 <a href="plugin.php?id=test:index&vote=225290&op=support" class="btn">支持</a> <a href="plugin.php?id=test:index&vote=225290&op=against" class="btn">反对</a> 该版主为了争取选票,他可以在帖子(甚至签名)中嵌入以下的代码: 一旦访问这个帖子,img中的链接即被访问一次,用户在“不知不觉”中投给了他一票。 反对这个版主的人也有办法,他可以在帖子(甚至签名)中嵌入以下的代码: 一旦访问这个帖子,同理,用户在“不知不觉”中投给了他一票反对。 我们当然知道,这种问题并不会造成非常大的影响,但当我们关注了足够多的严重安全漏洞后,这些小小的漏洞是否值得注意呢?它们的确影响到了插件的初衷。 我们接着讲一种面向管理员的CSRF,这种方法往往需要一定的运气,后台程序也有可能,但是后攻击难度太大。 假设刚才的投票插件,有一个前台管理面板,里面自然应该有一个“删除该候选人”的功能,我们假设一下。 225290 xxx xxx票支持 xxx票反对 查看投票数据 | 删除该候选人 我们假设“删除该候选人”是这样的一个链接 <a href="plugin.php?id=test:cp&action=del&uid=225290">删除该候选人</a> 假设前台面板是这样的执行代码: if($_GET['action']=='del' && $_G['adminid']==1 && ($uid=intval($_GET['uid']))>0){ …… } 这样的话,有一种非常需要运气的方法,只要让有权限的用户访问到“plugin.php?id=test:cp&action=del&uid=225290”这个链接,即可将225290这个候选人删除掉,这里对管理员权限有要求,但是这是可以实现的,实战中有一个例子: 版块:站务管理 标题:管理员进来!网页有错位! 内容:…… . 一般稍有管理的论坛,都会有管理员级用户访问。管理员可能永远不知道攻击是如何发生的,但是他已经实施了删除操作。这就是典型的面向管理员的CSRF攻击。 特别是删除操作,往往判断不足,使这个便成了高热度的情况。 修补建议: 1. 使用formhash formhash是比较简单有效的一种方法,比如在链接中加一个formhash={FORMHASH}就有效果,在模板语法中,我们可以这样写 <a href="plugin.php?id=test:cp&action=del&uid=225290&formhash={FORMHASH}}">删除该候选人</a> Discuz! 0629补丁所修复的一个安全问题涉及security key的取出,formhash的安全性被降低,在了解指定用户的情况下可能制造formhash,但是利用0629漏洞的例子还比较少,这个漏洞有远古性,0day工具也没有释出,了解对方环境有一定难度,所以formhash还是可信的。请放心使用这个方法! 2. 使用submitcheck() submitcheck有两种好处,一种是确定通过POST的方式提交了一个变量,另一种是对来源地址有审查,也就避免了站外的访问威胁。这在表单中,我认为是有必要添加的。submitcheck同时也会检查formhash,并且防止FLASH攻击。 一般是在添加/删除表单使用的。例如有一个添加候选人的表单,提交按钮的name="addsubmit"。 PHP代码中可以这样写: if(submitcheck('addsubmit')){ …. } 这样,提交时我可以肯定四个要点:POST提交,有FORMHASH,来源有限,不是FLASH,安全性能提高很多。如果要对删除操作进行这个检查,通常是通过弹出窗口进行confirm,用formhash的安全性也能满足一般需要。 Part 11. ceil round floor的区别一般是针对积分兑换的插件,我举一个例子,如果规定50积分可以换一个某奖品。 $a['奖品']=$func($a['积分']); 使用 ceil 函数,我可以支付51个积分,换两个(进一法) 使用 round 函数,我可以支付75个积分,换两个(四舍五入) 使用 floor 函数,我只能至少支付100个积分,换两个(舍去法) 这是三个取整函数,特别注意根据实际需要,使用适当的函数,避免让一些用户破坏了积分兑换的公平。或者,你的程序应该自己判断,使用户提供的积分中不足额的部分不被扣减。 在此处,intval相当于 floor,也可以。 Part 12. 不要上传SWFFLASH CSRF已经成为非常恐怖的攻击力量,在这里的建议是 不要允许用户上传SWF格式的文件,一旦这种文件上传到论坛上,就可以引发FLASH CSRF攻击。 这种攻击的威胁非常巨大,如果普通用户访问,可以攻破FORMHASH和submitcheck(),如果管理员访问,可以攻破!defined('IN_ADMINCP') && die; 原理就是利用FLASH请求数据,并且发出getURL的请求,或者javascript到浏览器上进行攻击,FLASH可以实施的攻击比较强。若自己的网页有必要展示swf时,请审慎控制allowScriptAccess参数。 Part 13. 不要Foreach空值、数字、字符串foreach一个空值或一个数字时,会报一个错误,这可能暴露路径,但新版中已对PHP错误报告做了处理。开发者需要在不引用Discuz来初始化的一些文件中注意。万一foreach一个不合格的内容时,会中断程序运行,使得程序执行不正常,这要注意。 Part 14. SESSION认证的验证码常见问题 并不推荐使用SESSION来做验证码,经过加密的cookie可以符合要求(例如使用Discuz!的authcode函数)。但SESSION也有其独特的优点,在这里我对用SESSION进行验证时,一种容易忽略的情况进行说明: example 17:<?php ... if(submitcheck(....)){ if($_SESSION['code']!=$_GET['seccode']) showmessage(...); ... }else{ ... session_start(); $_SESSION['code']=random(4, 1); ... ?> 这里出现一个问题,若用户不接受SESSION,那么在submitcheck时,$_SESSION['code']为空,seccode为空时绕过。即是,如果你用SESSION进行验证,一定要检查SESSION是否真的存在并且有效,避免直接使用SESSION的存储值而忽视SESSION是否存在的问题,使得攻击者可以轻易绕过验证。 DX论坛及插件机制特色Part 15. 伪造IP伪造IP是从很久以前就有的一个安全问题了,绝大多数的网站都不能正确处理,使得该伪造方法可以畅通无阻。如果有需要发敏感文章,那么通过伪造IP,甚至可以让人做替罪羊。 我们来看一下 Discuz! X 判断用户IP的方法。 private function _get_client_ip() { $ip = $_SERVER['REMOTE_ADDR']; if (isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; }elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) { foreach ($matches[0] AS $xip) { if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) { $ip = $xip; break; } } } return $ip; } 我们可以看到, 获得IP的优先级是 CLIENT-IP、 X-FORWARDED-FOR, 这两个是在 HTTP 头特别指定的量,相当于“识别真实IP”,这是在中国应用非常广泛的代码。 但是,逆向过来看,你如果提交 X-FORWARDED-FOR: 127.0.0.1 ,Discuz就会认为你是来自 127.0.0.1 的用户,难道我说什么,DZ就信什么?你还真说对了。已确定 Discuz! 除了限制代理访问之外,没有其它方法阻止此类假IP。此方法可用于刷Discuz!的访问推广的积分。 开发者编写投票插件,建议使用以下的代码进行判断,使得有代理迹象的投票全部无效。 if($_SERVER['HTTP_X_FORWARDED_FOR'] || ERVER['HTTP_VIA'] || $_SERVER['HTTP_PROXY_CONNECTION'] || ERVER['HTTP_USER_AGENT_VIA'] || $_SERVER['HTTP_CACHE_INFO'] || ERVER['HTTP_PROXY_CONNECTION']) { 这是代理投票,在此填你的处理代码 } Part 16. 后台模块为什么一定要加IN_ADMINCP?在X版本中,在插件目录下的任意以 .inc.php 结尾的文件都可以通过 plugin.php?id=xxxx:xxxx 的形式访问,不管这个文件有没有登记为模块,以及是否是后台模块,也可以通过这种方法被访问。 我们假使有这样的一个插件,它的目录分布如下: template 模版目录 admincp.inc.php 后台管理文件(包括修改、结单、删除等功能) index.inc.php 主程序(包括插件数据的初始化,cp.inc.php的权限判断) install.inc.php 安装文件(在XML中已登记) uninstall.inc.php 卸载文件(在XML中已登记) cron_payrecord_bakup.inc.php 计划任务文件(由install.inc.php在安装时复制到cron文件夹中,请注意这种做法已经不再允许) 登记的插件模块 管理中心 admincp.inc.php 支付管理 主导航项目 index.inc.php 主导航项目 我下面给出几个链接 plugin.php?id=xxxx:admincp 在admincp.inc.php没有IN_ADMINCP或者强制权限判断的情况下,我可以通过这个程序进行一些关键的操作(修改、结单、删除),其中的formhash和submitcheck都可以被绕过。除非碰到showformheader等函数被迫终止。 plugin.php?id=xxxx:cp 该程序的一些变量没有经过index.inc.php初始化,可能功能失常。当权限判断由index.inc.php承担时,权限判断被绕过。 plugin.php?id=xxxx:index 正常访问的连接 plugin.php?id=xxxx:install 插件进入安装过程,但因为runquery函数是在function/plugin中定义的,平时不被引用,反而很难清空数据库。提议安装文件严格命名为install.php plugin.php?id=xxxx:uninstall 同xxxx:install,一般不能进行卸载攻击,但是也可能造成其他风险。 plugin.php?id=xxxx:cron_payrecord_bakup 运行计划任务程序,咋看问题不大,但是有两点。一些计划任务有明确的时间性,这个连接可能使计划任务在短时间内被重复运行,类似定期发奖类的可能混乱。第二,计划任务很多比较消耗资源,通过计划任务实施DDOS或者CC攻击,达到的效果会快而且好。 重申不再允许插件安装时复制/移动文件到论坛程序文件夹,X3应该直接放在插件的cron目录下,X2.5应该同时“合并安装”一个计划任务扩展。 建议:后台管理文件一定要记得加 IN_ADMINCP,无需前台访问的文件不要加.inc.php Part 17. 敏感词过滤大家对Discuz! 的那个敏感词过滤不应太有信心的 插件中,如果有推广类信息,并且附带有敏感词过滤功能的,建议作考虑,不允许用户使用统一码表示汉字。 Part 18. 手机版和打印版改变了局面近年出现很多验证类的插件,经过调查,多数没有抵御灌水机的能力。因为较新的灌水机都会优先使用wap手机版页面进行灌水,这样可以绕掉验证类插件的电脑版嵌入点。 由于有用户投诉,后期统一对没有针对手机版设计嵌入点,又声称用于防灌水的验证类插件全部给予打回处理。只是用于检查用户提供的手机是否有效的这一类验证,都放宽。 之前有很多回复限制类插件,限制用户需要达到一定的条件才能访问帖子。有登陆看全文的,也有回复看全文的。暂不提其中一些产品严重影响了搜索引擎的收录,并且可能被指出是欺骗。这些插件很多在打印版/手机版/archiver下失败了,显然嵌入点少了,难以实现这些功能。 我们建议这类插件能设法showmessage来屏蔽打印版,同时在$postlist里面先下功夫,这样在archiver下应该可行。 Part 19. 网址路径书写不合理 近期有多款插件/风格因此被打回,开发者具体的测试环境可能有所不同,使得开发者debug时的显示效果和全部用户网站上的显示效果有差异,网址路径书写不合理是其中的一种。 第一种情况是:路径使用反斜杠 正确:<img src="source/plugin/test/static/test.jpg" /> 错误:<img src="source\plugin\test\static\test.jpg" /> 并非所有的服务器都接受反斜杠,请不要用反斜杠书写路径。 第二种情况是:路径前加一个/,使路径从根目录开始 正确:<img src="source/plugin/test/static/test.jpg" /> 错误:<img src="/source/plugin/test/static/test.jpg" /> 当用户的BBS并非安装在根目录时,图片文件不能被正确引用。 Part 20. 调用远程信息的可靠性过滤 开发一些插件时,我们会通过客户端的页面向我们插件的服务器端发送一些信息,例如站长信息,信息调用,或者功能请求。一个常见的例子便是更新信息。诚然,我们放心自己不会往自己的服务器端挂马,但是如果域名被劫持,或者发生这一类的事故,那么利用者就可能通过这个插件后台端向使用者发送攻击信息。Discuz!某版本的挂马,同时利用了后台的一个执行脚本的漏洞,以及custom.discuz.net的劫持。 因此,尽管向用户端发送的信息由我们控制,我们依然希望里面没有有害代码。这是一个提议,建议服务器端发送的代码,尽可能由不需HTML标记的代码组成,以便htmlspecialchars处理。或者,直接使用iframe的方式引用,较新的浏览器都有良好的权限控制,使得框架内的代码无法控制框架外的主页面。请用iframe而不要用javascript,后者可以跨站。 Part 21. IN_DISCUZ的必要性 至今仍有不少插件由于缺少必要的IN_DISCUZ而被打回,缺少IN_DISCUZ的程序被单独执行时,往往由于没有初始化函数,而报出fatal error泄露服务器路径。实际操作中,确可无需IN_DISCUZ的类文件和函数文件并不做强制性要求,但建议只要是可以加这项限制的,都应该加,这样比较合理。 Part 22. 认证类密串的生成问题 有时,为了校验用户的身份,或者验证用户的权限,我们会通过网址传递一个hash,hash正确的才执行相关代码。 plugin.php?id=xxx:xxx&ac=openimg&id=1893&hash=b95970a598bc1da9af743f988d9b0ee0 其hash的生成算法是 <?php ... $hash=md5('*cOpYRight*'.$imgid.'|'.$_G['uid'].'|'.$_G['siteurl']); ... ?> 从生成的md5来反推原文基本是不可能的,但是生成这样一个md5却容易。即使你的插件使用了Zend加密,至今依然有各种方法可以反编译,并了解到你生成hash的算法。由于算法与论坛相关的部分只有siteurl,而siteurl很容易得到,所以这个hash实际上可以由客户端自行生成。 显然,要解决这样的问题,我们要让这个hash不容易生成,即使知道加密算法。我们可以在md5的参数中增加$_G['authkey'],或者直接用formhash($imgid) 也是可以的。 一般的认证场合中,直接使用FORMHASH也可以,除非这个hash不止需要与用户的身份有关,例如还与附件的ID有关,等等。 Part 23. 多编码版本的程序编码问题 目前,大多数的插件都使用语言包编写,并且支持多编码。这就要求汉字和汉字符号,都应该写在语言包内。最近有不少插件,在php文件中仍然有非注释部分的汉字和汉字符号,都让作者拿回去改,或者用统一码&#xxxx;这样的来解决。这样,不同编码的用户才可以真正看到相同的效果。 Part 24. 由SWF决定奖励的抽奖插件 许多抽奖插件给论坛带来了许多娱乐享受,这些插件有很大的缺口,我觉得有兴趣的开发者可以试试看。有不少的抽奖插件由swf抽奖,决定奖品后发送给服务端,由服务端发奖。这就涉及到了解swf与服务端的通讯方式后,恶意刷奖品。我们提醒,swf发奖插件应该由php决定奖品,swf仅负责显示奖品。 Part 25. 时间安全问题 在TIMESTAMP常量产生后,Discuz!会进行timezone_set,此后用date(),time(),gmdate(),dgmdate()获得的时间由系统及用户共同决定,除非你使用date(..., TIMESTAMP) gmdate(...,TIMESTAMP)等。其中,用户设置的时区可以远远超过24小时。如果插件中用于判断时间的代码没有考虑到这个问题,就可以使用户穿越时空。例如一款签到插件,在早期某版本中,许多非开发者用户已经知道通过调整时差,就可以向前一天或者向后一天。而实际上,如果该用户通过DOM编辑器等方式,提交了一个超过24小时的时差,那么该用户可以穿越到任何一天,这就使得签到用户可以补签之前任何一天的记录,也可以签到未来的日子。 同样的问题如果出现在银行插件,那么后果会更加严重。可喜的是应用中心唯一一款银行插件,使用TIMESTAMP之间的对比,这样就回避了这类问题。
文件下载:
|