这是工作记录,慢慢的不知不觉就形成篇文章了,就发在这里吧。

-- Split --

acookie 就是段统计代码,它的原理就是发送个 GET 请求到统计服务器,并附上本地用户的浏览器类型、分辨率等其他信息。

问题

有人报告 acookie 的代码出现 XSS ,原理是利用某 GET 参数 bypass 未过滤完全的字符。这里是原先的代码摘录:

(function() {
    function akrand(num) {
        return Math.floor(Math.random() * num) + 1
    }
    var P = location.pathname;
    if ((parent === self) || P.indexOf('{...}') != - 1 || P.indexOf('{...}') != - 1) {
        var R = escape(document.referrer);
        var id = "" + akrand(9999999) + akrand(9999999);
        var title = escape(document.title);
                document.write('<img src="http://foo/1.gif?acookie_load_id=' + id + '&title=' + title + '&pre=' + R 
                       + '&param0={...}&param1={...}&param2={...}"  width="0" height="0" style="display:none;" />');
    }
})();

这段代码「年事已高」每次的修修补补都是治标不治本,于是干脆考虑重写这段统计代码。

目标

在开始重写之前,为将要写的代码列了几条目标:

  1. 流量方面的考虑,代码要尽可能的简短
  2. 尽可能的优化性能
  3. 「绿色」,不干扰页面的其他脚本
  4. 脚本尽可能得做到安全

思考

  1. 使用原先的 document.write 会在页面中加入 DOM,直接使用 new Image 更好
  2. 原先的判断条件很冗余,完全可以考虑个函数搞定
  3. 安全方面虽然可以写个简短的过滤函数,但这样代码又会很长

解决

下面是反复修改后的最终代码:

(function(){
    var M = function(n) {
        return Math.floor(Math.random() * n) + 1;
    },
    I = function() {
        for (var i = 0, P = location.pathname, args = arguments, len = args.length; i < len; i++) {
            if (P && P.indexOf(args[i]) !== -1) {
                return 1;
            }
        }
        return 0;
    },
    D = document, R = escape(D.referrer), S = screen, T = escape(D.title);

    if (parent === self || I('{...}') || I('{...}')) {
        try {
            return new Image().src = [
                "http://foo/1.gif?cache=" + M(9999999),
                '&pre='+ R +'&scr='+ S.width +'x'+ S.height + '&title=' + T,
                '&param0=' + '{...}',
                '&param1=' + '{...}',
                '&param2=' + '{...}'
            ].join('');
        } catch (e) {}
    }
})();

本来想使用 ~function() {}; 这样的闭包, 结果发现效率方面还是原先的理想 (当然不会差很多),加之可能以后阅读的同事会有困扰,还是采用原先的吧。

在这个案例中,其实返回 0 和 1 同比返回 false 和 true 是同样的道理,处于节省代码量考虑,直接使用整型值。

安全方面出于简单原则考虑,没有考虑使用转义函数,而是采用数组拼贴的方式,这样就算有注入也会造成语法报错而不能指定此段脚本,前面加 return 也是这样考虑。

后记

考虑以后的代码可维护性很重要,这个例子中代码虽然简单,当然还会有更极端的写法,不过处于以后同事的合作考虑,尽量不要写得过于的「专业」。

采用 document.write 的考虑是想直接使用脚本在页面上输出,因此在本案例中不是很适用,同时才用这一方法在安全的角度上考虑需要过滤的字符太多,因此如果了解需求(比如仅仅是生成个带参数的 URL)还是避免使用它

XSS 和 CSRF 等虽说后台处理是治本的,但如果使用不当前后台考虑不周全就算加入了对应的过滤函数,也有可能造成注入。也从另个角度考虑,有时候完善的代码也能从一定程度上加大注入的难度,尤其是 Javascript 等这种「每个人都可以看见得脚本」。

-- EOF --