1.1 利用FormHash防CSRF和变淡自动提交
FormHash 指的是,通过在 Form 表单中构造一个隐藏的 input 元素,如:
让第三方难以伪造这个 input 的 value,借此阻止网站外部随意构造表单提交,即防CSRF。适合的业务场景有注册、登录、下单、秒杀、抽奖、积分换代金券等等。
1.2 通过全局 Filter 检测或过滤 XSS/SQLi/shell注入
最好是在框架层面增加全局 Filter,对 HTTP Request 中的 $_GET/$_POST/$_COOKIE 进行字符串过滤,这种 Filter 将是强制性的。(出于防范CSRF(Cross-Site Request Forgery,跨站请求伪造)的考虑[注4],工程师尽量不要使用 $_REQUEST。)
对于 PHP 来说,还可以在开发环境引入 laruence 的检测 xss/sql/shell 注入的 PHP 扩展模块。
其次,杜绝裸写SQL。
1.2.1.大的原则是:不要相信客户端提交的数据
要深刻理解XSS的原理,攻击代码不一定(非要)在 中。
所以,处理XSS注入的时候,不仅仅要转义或删除特殊的 HTML 标记和符号,如尖括号<>,如script,如iframe等,还需要过滤 JavaScript 事件所涉及的大量属性.
否则,就可能出现下面这两种的XSS漏洞实例:
例1:
http://YourDomain.com/index?num=1%22+onmousemove%3Dalert%284%29+wb%3D%22
%22就是单引号的转义,%3D是等号,%28是左括号,%29是右括号。鼠标移动可触发。
例2:
访问 http://YourDomain.com/;
在页眉的搜索输入框中输入关键词:" onmousemove=alert(4) wb=" (注:包含双引号);
在搜索结果页面上,当鼠标移动时,就会弹出XSS种下的JS弹出框
1.3.验证码服务不简单
图片验证码或者答题验证码是为了尽可能狙击注册机、秒杀器等非人类行为,自然就成了攻防第一线的重要技术。
攻:提前收集验证码。适用场景:秒杀。在秒杀开始前就收集好验证码,从而提前准备好表单。
防:1)不同业务的验证码不得混用,登录、注册、抽奖、秒杀、下发短信验证等各是各的。2)像秒杀这种与时间有关的业务场景,验证码就不允许提前生成。即使刻意构造出某个秒杀活动的图片验证码URL访问,也必须先判断秒杀活动开始时间,阻止提前访问。
攻:构造表单时使用过期 cookie。
验证码一般是浏览器提交 cookie 里的 verifysession(一个标识本次验证会话的 Hash 串)和手工输入的验证码字符串 verifycode,服务器端按 F(verifysession)==verifycode 进行校验。那么秒杀器突破时,别用服务器之前响应的 Hash 串,一直用自己手中掌握的 verifysession 和 verifycode 提交,就可以突破了。即使这一组 verifysession 和 verifycode 在服务器端验证后立即失效,也无所谓,多准备几组数据,就拼谁的秒杀器并发提交快即可。
防:1)结合 FormHash 方法联防。2)针对不同业务生成的验证码的过期时间可以单独设定。可以让某个业务的验证码图片过期时间很短。
攻:OCR识别图片验证码。
防:如果业务场景确实非常需要狙击机器提交,那么上答题验证码,但也因此有了“题库有限”这个弱点。
攻:(既然题库有限那么)通过刷秒杀页面来收集题目。然后人工快速回答,或者 OCR 识别问题内容,自动匹配答案。
防:FormHash 有一定作用,但对于“按键精灵”软件录制脚本(而不是机器构造表单),确实难以防范,第三方只要比真正的用户响应快、提交快即可。
攻:答题时,构造表单使用过期题目。
原理类似于使用过期 cookie。如果本次秒杀商品对应的答题不是一对一锁定的(即换其他题提交也可以),那么也可以强制使用过期题目。
防:加Token。让 Token 的生成与业务的具体场景有关。如一个 Token 由登录用户(以user id标识)和当前秒杀商品(以goods id标识)等关键信息生成。也是 FormHash 思路的延续。或者当前秒杀绑定题目,禁止使用其他题目答题。
攻:上面的措施都挡不住我。
防:拦人。对于黑名单用户(手动加或自动识别),他能参与秒杀,但总是得到如下提示“系统繁忙:人太多了,休息一下,等等吧”。
其他电商的做法参考郑昀的《对付秒杀器等恶意访问行为的简单梳理》 [注6] 。
1.4.敏感信息存入 cookies 须防篡改
电商应用有时候不可避免地存储了一些敏感数据到客户端,当然不希望被客户端篡改。
所以可以在 set-cookie 时加上防篡改验证码,或者不暴露原值直接对称加密存储。
如:user_name=alex|bj95ef23cc6daecc475de
防篡改验证码的生成规则可以很简单:md5(cookieValue+key)或sha1(cookieValue+key),key可以是服务器端掌握的一个固定字符串,也可以很复杂(如LTPA示例[注7])。
1.5.IP地址防伪造
有人会说可以通过 request.getHeader("X-FORWARDED-FOR") 获得ip字符串,这个头域简称 XFF头,只有在通过了 HTTP 代理或者负载均衡服务器时才会添加该项。
但一定要知道 XFF 头仅仅是 HTTP Headers 中的一分子,那自然是可以随意增删改的。很多投票系统都有此漏洞,它们简单地取 XFF 头中的ip地址,设置为客户端来源地址,因此第三方可以伪造任何ip投票。
所以规则一:不要轻信 HTTP Headers 里的IP字段。
还与客户端到服务器端之间的路径有关,所以工程师要关注生产环境里你的 WebServer 前面到底挂的是什么,Nginx 的 proxy_set_header 是怎么配置的。
几种场景:1)F5->Nginx->WebServer;2)F5->WebServer;3)Client->CDN->Nginx。搞清楚场景和配置,拿到真正的 Remote Address,请参考郑昀的《客户端的IP地址伪造、CDN、反向代理、获取的那些事儿》。
1.6.防 Session Hijacking
当第三方以如下手法获得了你的浏览器标识会话的字符串,那么他也许能以你的身份冒名操作:
从访问记录里获得 URL 上携带的 Session Token;
利用 XSS 漏洞窃取 Cookie 里存储的 Session Token。
所以,首先绝不在 URL 中传递 Session Token,除非走 HTTPS 通道。
其次,需要多管齐下,1)cookie 里的 Session Token,必须设定为 httponly,禁止 JavaScript 读取,以绝后患;2)如果服务器端还需要 cookie 里存储的用户ID等信息,那么 cookie 键值需要加签名防篡改;3)根据客户端的IP地址、User-Agent、Session和其他信息生成一个 UA Token,存储在 cookie 里;服务器端每次都会核对这个 UA Token 是否正确,如果不正确则退出登录。这样,即使第三方拿到了你的 Session Token,也无法处于登录状态。
1.7.用 Robots.txt 限制住搜索引擎
每一个对公网暴露的 Web 工程,上线之初理应配有 Robots.txt ,不仅仅是为了SEO,而且要限制站点的某些目录不允许抓取和收录。
内部站点的robots.txt 内容必须(MUST)是:
User-agent: *
Disallow: /
不少浏览器都会向搜索引擎传送访问历史,你以为的内部隐秘访问地址,可能早已被搜索引擎收录了,所以对此绝不要掉以轻心。不要给入侵者借搜索“site:YourDomain.com”或“site:YourDomain.com URL:/YourPath” 遍历内部站点的机会。
1.8.敏感信息的存储和展示
牢记一点,你的数据库很有可能被拖库,我们要未雨绸缪,降低拖库后的危害!
所以,敏感信息请加密存储。
1.8.1.密码的存储
首先,绝不能仅仅 MD5(password) 存储,这样等同于明文存储。其次铁律是,禁止明文存储(登录)密码。
最简单的办法是,随机生成 SaltKey 并存储,按 MD5(salt+MD5(password)) 存储密码。
如果密码需要二次分发(而不是重置密码),请对称加密存储(应用程序掌管公钥私钥)。
1.8.2.敏感信息的展示
牢记一点,第三方很有可能拿到你的平台登录权限(通过 session hijacking,或通过内部人),所以要未雨绸缪,敏感字段不要轻易示人!
所以,敏感信息尽量不要明文展示,即使是合法用户登录状态下。
以下信息需“中间若干位星号显示”,如果没有特别理由,那么默认不能直接显示(包括导表):
(商户的)银行帐号,
(商户的或用户的)手机号码和邮箱地址。
1.10.防表单重复提交
有两个含义:
页面无刷新情况下,某些业务场景需要主动阻止表单重复提交:
有的业务场景支持重复提交但会提示:如实物类电商,同一个 SKU 提示重复加入购物车、但仍能加入购物车,只是在相同 SKU 上加数量;
有的场景会通过以下手法尽量提前避免表单(尤其是数据未变化的情况下)重复提交,而不用非要到服务器端在数据库层面做重复数据判断:
按钮点击后变灰(按钮文字可以更改,如显示倒计时),收到 Ajax 响应后再恢复,典型场景是填写手机号码点击按钮下发短信验证码时;
页面刷新情况下,尽量阻止表单重复提交:
用户行为有:
用户按F5刷新(注意,不是Ctrl+F5强制刷新);
用户点击浏览器的后退或前进功能回到之前的网页;
这时需要服务器端配合了:
POST ****表单提交后,服务器端做302跳转,利用302跳转清空表单参数;
延续 FormHash 思路,服务器端收到 formhash 值后存入缓存,几分钟后过期,这样校验逻辑可以阻止几分钟内的含相同 formhash 值的表单重复提交。
1.11.访问速率限制Rate Limits
业务限流的目的是:
防扫号防暴力破解场景:防止对手高速扫描(或调用)我们的系统某个URI。这种场景经常发生在凌晨,对于异常访问,我们系统必须第一时间拦截,在这种情况下,可能不允许太高的突发。
保证系统平稳运行:系统承载能力有限,数据库支撑能力有限,所以不希望系统由于突发陡增请求而引发下游系统性能出现凸起,当然适量的波动是允许的。
工程上有几种做法:
1.11.2.简单的memcache以秒为单位的度量
memcache 里的 key 可以为:业务编号IP地址时间戳_Limiter种类。时间戳精确到秒或分钟。
1.11.3.漏桶(Leaky Bucket)算法
Nginx 的 limitReq 模块采用的就是漏桶算法,分 delay 和 nodelay 两种处理模式
1.11.4.令牌桶(Token Bucket)算法
令牌桶具体实现:
在数据结构上,没有必要真的实现一个令牌桶。
基于时间的流逝生成受控制数量的令牌即可——以时间的流逝来洗涤旧迹,也就是将两次发包或者收包的间隔和令牌数量联系起来。
令牌桶和漏桶算法最主要的差别在于:漏桶算法能够强行限制数据的传输速率,而令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输。
相对于漏桶算法,令牌桶的波动幅度可能是我们系统无法承受的。
令牌是基于 Request 触发投放到令牌桶的,如果是按照下面的投放策略:
delta = self.fill_rate * (now - self.timestamp)#fill_rate is the rate in tokens/second that the bucket will be refilled.
self._tokens = min(self.capacity, self._tokens + delta)
那么代入演算一下,令牌桶令牌总数 capacity=80,还剩余0张令牌,令牌桶填充速率fill_rate=0.5t/s,流逝的时间是10秒,即过去的10秒没有任何请求,第11秒突然拿来了一个请求,于是令牌桶里就会放入min(80,5)=5张令牌,意味着第11秒可以消耗5个令牌。
这也就是我们为什么说令牌桶算法是“只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的上限,因此它适合于具有突发特性的流量”的缘故。
了解更多漏桶和令牌桶算法信息,请参考郑昀的《集群环境下业务限流[注10]》。
1.11.5.滑窗(Sliding Window)算法
淘宝中间件博客上曾提及,『流量预警和限流方案中,滑窗模式通过统计多个单元时间的访问次数来进行控制,当单位时间的访问次数达到某个峰值时进行限流。
在每次有访问进来时,我们判断前N个单位时间里总访问量是否超过了设置的阈值,若超过则不允许执行。
缺点一:
由于访问量的不可预见性,会出现单位时间的前半段有大量请求涌入,而后半段则拒绝所有请求的情况发生。
缺点二:
其次,我们很难确定这个阈值设置在多少比较合适,只能通过经验或者模拟(如压测)来估算。不过即使是压测也很难估计准确。
所以滑窗模式往往用来对某一资源的保护上(或者说是承诺比较合适:我对某一接口的提供者承诺过,最高调用量不超过XX),如对 DB 的保护,对某一服务的调用的控制上。
代码实现思路如下:
每一个窗(单位时间)就是一个独立的计数器(原子计数器),用以数组保存。将当前时间以某种方式(比如取模)映射到数组的一项中。每次访问先对当前窗内计数器+1,再计算前N个单元格的访问量综合,超过阈值则限流。
这里有个问题,时间永远是递增的,单纯的取模,会导致数组过长,使用内存过多,我们可以用环形队列来解决这个问题。』