土薯工具 Toolshu.com 登录 用户注册

如何检测第三方 JavaScript 脚本对全局作用域的污染?——实战监控 window 变量变化

原创 作者:bhnw 于 2025-11-10 15:49 编辑 23次浏览 收藏 (0)

在现代 Web 开发中,我们经常会引入第三方脚本——广告、统计、SDK、插件等。这些脚本为了“方便”,常常直接向全局作用域(window 对象)注入变量、函数甚至修改原生对象。这种行为不仅会污染全局命名空间,还可能导致变量冲突、性能下降、安全风险,甚至悄无声息地篡改关键 API(比如重写 console.logXMLHttpRequest)。

那么,作为开发者,我们如何准确识别一个第三方脚本到底往 window 上加了什么、改了什么?本文将带你手把手实现一个轻量级但可靠的监控方案。


一、问题场景

假设我们要加载一个广告脚本:

<script src="https://cdn.wwads.cn/js/makemoney.js"></script>

我们想知道:

  • 它新增了哪些全局变量?
  • 它是否修改了已有的全局属性(比如 ArrayPromise 或自定义变量)?

仅靠 Object.keys(window).length 只能知道数量变化,无法定位具体内容。我们需要更精细的对比。


二、核心思路:快照 + 差异对比

要检测“变化”,最直接的方法是:

  1. 加载脚本前,对 window 做一次完整快照(属性名 + 值)。
  2. 加载脚本后,再做一次快照。
  3. 对比两次快照,找出新增项值发生变化的项

关键点:

  • 使用 Object.getOwnPropertyNames(window) 获取所有属性(包括不可枚举的)。
  • 使用 Map 存储属性名与值的映射,便于高效查找。
  • 使用 Object.is() 进行值比较,避免 NaN-0/+0 等边界问题。
  • 包裹 try...catch,防止访问某些特殊属性时抛出异常。

三、完整实现代码

// 1. 创建 window 快照:记录所有属性及其当前值
function snapshotWindow() {
  const props = Object.getOwnPropertyNames(window);
  const snap = new Map();
  for (const key of props) {
    try {
      snap.set(key, window[key]);
    } catch (e) {
      // 某些属性(如跨域 iframe 中的 opener)访问会报错
      snap.set(key, '<ACCESS_ERROR>');
    }
  }
  return snap;
}

// 2. 记录加载前的状态
const beforeSnapshot = snapshotWindow();

// 3. 动态加载第三方脚本
const script = document.createElement('script');
script.src = 'https://cdn.wwads.cn/js/makemoney.js';

// 4. 脚本加载完成后进行对比
script.onload = () => {
  // 稍作延迟,确保脚本完全执行(含微任务)
  setTimeout(() => {
    const afterSnapshot = snapshotWindow();

    const added = [];
    const modified = [];

    // 遍历加载后的所有属性
    for (const [key, newValue] of afterSnapshot) {
      if (!beforeSnapshot.has(key)) {
        added.push(key); // 新增
      } else {
        const oldValue = beforeSnapshot.get(key);
        if (!Object.is(oldValue, newValue)) {
          modified.push({ key, oldValue, newValue }); // 被修改
        }
      }
    }

    console.log('🟢 新增的全局变量:', added);
    console.log('🟡 被修改的全局变量:', modified);
  }, 100);
};

script.onerror = () => console.error('第三方脚本加载失败');
document.head.appendChild(script);

四、输出示例与解读

运行上述代码后,控制台可能输出:

🟢 新增的全局变量: ['_AdBlockInit', '_IsTrustedClick']
🟡 被修改的全局变量: [
  { key: "isMobile", oldValue: [Function], newValue: [Object] }
]

这意味着:

  • 脚本注入了 _AdBlockInit 等广告相关变量;
  • 它可能重写了 isMobile 对象

这类行为在广告或数据采集脚本中并不罕见,但对应用稳定性和安全性构成潜在威胁。


五、注意事项与进阶建议

1. 动态属性误报

某些 window 属性是 getter(如 window.devicePixelRatio),每次读取值可能不同。这类属性可能被误判为“修改”。可建立白名单过滤:

const DYNAMIC_PROPS = new Set(['devicePixelRatio', 'innerWidth', 'innerHeight', 'scrollY']);
if (DYNAMIC_PROPS.has(key)) return; // 跳过对比

2. 深度修改无法检测

如果脚本只修改了某对象的内部属性(如 window.myLib.config = {...}),而 myLib 引用未变,则不会被识别。如需深度监控,可结合 Proxy 或定期遍历,但代价较高。

3. 生产环境慎用

此方案主要用于开发调试或安全审计,不建议在生产环境长期运行,因其会遍历整个 window,有一定性能开销。

4. 结合 CSP 与沙箱

对于高安全要求的场景,建议:

  • 使用 Content Security Policy (CSP) 限制脚本来源;
  • 将第三方脚本运行在 iframe 沙箱 中,彻底隔离全局作用域。

5. 隔离方案:运行后自动恢复关键全局变量

如果你已知某些第三方脚本会覆盖特定全局变量(如 isMobileconfig),但又无法避免加载该脚本,可以采用“备份-恢复”策略,在脚本执行后自动还原关键变量,避免副作用扩散:

function loadThirdPartyScript(src, backupGlobals = []) {
  const backup = {};
  backupGlobals.forEach(key => {
    backup[key] = window[key];
  });

  const script = document.createElement('script');
  script.src = src;
  script.async = true;

  script.onload = script.onerror = () => {
    backupGlobals.forEach(key => {
      window[key] = backup[key];
    });
  };

  document.head.appendChild(script);
}

// 使用示例
loadThirdPartyScript('https://cdn.wwads.cn/js/makemoney.js ', ['isMobile', 'config']);

⚠️ 注意:此方法仅适用于脚本同步执行完毕后立即产生副作用的场景。若脚本通过 setTimeout、事件监听器或异步回调持续修改全局变量,则恢复操作可能失效。


六、总结

第三方脚本如同“黑盒”,我们无法控制其实现,但可以主动监控其副作用。通过简单的快照对比,就能清晰看到它对全局环境的“入侵”行为,为性能优化、安全审计和故障排查提供有力依据。

下次当你引入一个未知脚本时,不妨先问一句:“你到底往 window 上写了什么?”


💡 小技巧:将上述逻辑封装为一个函数 detectGlobalPollution(scriptUrl),可复用于任何第三方脚本的检测。

发现周边 发现周边
评论区

加载中...

红包