DOM XSS攻击的那些坑我替你们踩过了

Top丶光远 安全 阅读 2,417
赞 27 收藏
二维码
手机扫码查看
反馈

DOM-based XSS防护:这几种方案我踩坑无数次了

说到DOM-based XSS,我真的是又爱又恨。前阵子在做项目安全加固的时候,被这玩意儿折腾得够呛。各种防护方案试了个遍,今天把踩过的坑都记录一下。

DOM XSS攻击的那些坑我替你们踩过了

其实DOM-based XSS和传统的XSS不太一样,它主要是在浏览器端执行恶意脚本,而不是服务器返回的内容。所以防护策略也要从客户端入手,常用的就那么几个:CSP、输入验证、输出编码、DOM操作限制。

这四种方案的核心区别

先说结论:我比较喜欢CSP + 输出编码的组合,这是最稳妥的方案。不过具体选择还是要看业务场景,毕竟每个方案都有自己的适用范围。

首先来看最严格的CSP(Content Security Policy),这玩意儿就是给浏览器定规矩的。你可以告诉浏览器哪些资源能加载,哪些不能加载。配置起来确实有点复杂,但是防护效果是最好的。

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://jztheme.com; object-src 'none';">

上面这个CSP配置就限制了脚本只能来自自身域名或者指定的CDN,基本杜绝了外部注入的可能性。但是说实话,这个配置真的很容易把自己也给拦住了,我就经常因为配错导致某些功能用不了。

再来说说输入验证,这个是最基础的防护手段。原理很简单,就是对用户输入的数据进行过滤:

function sanitizeInput(input) {
    if (typeof input !== 'string') return '';
    
    // 移除script标签
    let sanitized = input.replace(/<scriptb[^<]*(?:(?!</script>)<[^<]*)*</script>/gi, '');
    // 移除javascript:伪协议
    sanitized = sanitized.replace(/javascript:/gi, '');
    // 移除on事件处理器
    sanitized = sanitized.replace(/s*onw+s*=/gi, '');
    
    return sanitized;
}

这个方案的问题是容易被绕过,比如大小写、编码、拼接等方式都能突破检测。而且维护成本高,每次发现新绕过方式都要更新规则。

输出编码是我比较常用的一个,特别是处理HTML插入的时候:

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// 使用示例
const userInput = '<script>alert("xss")</script>';
const safeOutput = escapeHtml(userInput);
document.getElementById('content').innerHTML = safeOutput; // 安全输出

这个方法相对可靠,但需要针对不同的上下文使用不同的编码方式,比如HTML属性、URL、CSS等都需要特殊的处理函数。

最后是DOM操作限制,这个主要是限制直接的DOM修改操作:

// 禁止eval和Function构造函数
window.eval = undefined;
window.Function = undefined;

// 使用安全的DOM方法
function safeSetContent(elementId, content) {
    const element = document.getElementById(elementId);
    if (element) {
        element.textContent = content; // 而不是innerHTML
    }
}

谁更灵活?谁更省事?

CSP的配置复杂度最高,但一旦配好了基本就不需要管了。我之前为了调试CSP配了整整一个周末,各种报错日志看得我眼花。不过它的好处是全局生效,不用在代码里到处加防护逻辑。

输入验证最灵活,可以根据具体的业务需求定制规则。但是维护成本也最高,每个输入点都要单独处理。我现在一般只在关键位置使用,比如搜索框、评论框等。

输出编码是我最常用的,因为它针对性强,不会影响整体流程。比如显示用户名的地方,我就会用textContent而不是innerHTML。这种方案改动最小,但需要开发者有安全意识。

DOM操作限制适合已经存在的老项目,可以通过限制危险方法来快速加固。不过这种方法治标不治本,如果代码本身就有问题还是会中招。

性能对比:差距比我想象的小

本来以为CSP会影响页面加载速度,结果测试下来几乎没差别。可能是现代浏览器优化得比较好,CSP检查基本都是毫秒级完成的。

输入验证和输出编码的性能消耗微乎其微,主要是字符串处理的开销。只有在处理大量数据的时候才稍微明显一些,但也在可接受范围内。

DOM操作限制对性能基本没影响,因为它只是替换了原生方法的引用。不过要注意的是,如果过度限制可能会影响正常的JavaScript执行。

我的选型逻辑

对于新项目,我会直接上CSP + 输出编码的组合。CSP提供全局防护,输出编码处理具体的XSS风险点。这样既保证了安全性,又不会给开发带来太大负担。

老项目的改造我会采用渐进式的方式,先加输出编码,再逐步完善CSP配置。毕竟老项目代码复杂,一步到位风险太高。

如果遇到特殊情况,比如需要动态加载外部资源的,我会在CSP里加白名单,同时加强输入验证。这时候就要权衡安全性和功能性了。

这里特别提醒一点,不要指望一种方案解决所有问题。DOM-based XSS的攻击方式多样,最好的办法是多层防护。我在实际项目中就是这样做的,CSP负责全局,输入验证处理入口,输出编码覆盖显示,基本上就没啥问题了。

踩坑提醒:这三点一定注意

第一,CSP配置错了会连自己网站都打不开。我就遇到过一次,配了一个很严格的策略,结果整个页面的JavaScript都不工作了。建议先在开发环境调试好,再推到生产。

第二,输入验证规则写得太松等于没写,写得太严又会影响用户体验。这个平衡点需要根据具体业务来调整,多测试各种边界情况。

第三,输出编码要在正确的时机进行。有些情况下需要在服务端编码,有些情况在客户端编码,搞错了反而会引入新的漏洞。

以上是我对DOM-based XSS防护方案的对比总结,有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论