🐈

Email Address の難読化 (Obfuscation) 機能

2024/11/05に公開

Webサイトに記載されるEmailアドレスは、サイト訪問者にとってウェブサイトの連絡先を示すい重要な情報です。一方様々なデータ収集ボットが動作しているインターネットにEmailアドレスを公開することは、フィッシング詐欺メールの対象になるなどリスクをはらみます。

IPA等の発表にもある通りEmailは引き続き企業にとって攻撃対象のトップとなっています。
https://www.ipa.go.jp/security/10threats/10threats2024.html

Email Address の難読化 (Obfuscation) とは

CloudflareはWAFや対DDoSテクノロジーの一環として高い精度でアクセスを行ったリクエスト送信者がブラウザ、つまり人によるアクセスなのか、ボットによるアクセスなのかを判別する機能を有しています。

この技術を転用しブラウザからのアクセスの際のみEmailを画面表示させる機能が、Email Address の難読化 (Obfuscation)です。企業によっては様々な対策をとっており、例えば画像でメールアドレスをHTMLに埋め込む等を行っていますが、この場合コピペが出来ないためユーザービリティは低下してしまいます。Cloudflareの機能を用いるとユーザーはコピペ可能なメールアドレスがHTMLに表示される一方でボットからのアクセスの場合は難読化された文字列となります。

JavaScriptを用いた難読化とは

この技術はJavaScriptを用いた難読化によって行われます。これはEmail以外にも一般的に使われるテクノロジーです。例えば以下のJavaScriptを見てみます。

難読化前
function greet(name) {
    alert("Hello, " + name + "!");
}

greet("Harunobu");
難読化後
(function(_0x3ab8b5,_0x2842b0){var _0x5e0b57=_0x3b16;while(!![]){try{var _0x210edb=parseInt(_0x5e0b57(0x156))/0x1+parseInt(_0x5e0b57(0x157))/0x2+parseInt(_0x5e0b57(0x158))/0x3+parseInt(_0x5e0b57(0x159))/0x4+parseInt(_0x5e0b57(0x15a))/0x5;if(_0x210edb===_0x2842b0)break;else _0x3ab8b5['push'](_0x3ab8b5['shift']());}catch(_0x221b4b){_0x3ab8b5['push'](_0x3ab8b5['shift']());}}})(_0x5c91,0xa40f1);
function _0x3b16(_0x46a2e3,_0x11b46a){return _0x5c91[_0x46a2e3-0x156];}
function greet(_0x39e927){alert('Hello, ' + _0x39e927 + '!');}
greet("Harunobu");

こうすることによってJavaScript実行環境を持たないボットでは画面に表示される文字列が取得できなくなります。勿論高度なボットはこの難読化をすり抜けますので、完璧なソリューションではないことに津は注意が必要です。

また攻撃者がウェブサイトにスクリプトを仕込む際は、開発者がパッと見ても何を行っているかわからないよう同じ技術が使われます。高度に難読化されたスクリプトは生成AIを活用してももとに戻すことは困難であり、何がブラウザ上で実行されているかを判別するには時間がかかります。

さっそくやってみる

マネージメントコンソール左ペインからScrape Shieldをクリックします。

Email Address Obfuscation のトグルをオンにします。

以下の記事に従いいつも通りオリジンを構築し、通信をCloudflare経由にします。
https://zenn.dev/kameoncloud/articles/6d2dec59232917
その後index.htmlを以下に変更します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello, World!</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>Contact: <a href="mailto:harunobu@cloudflare.com">harunobu@cloudflare.com</a></p>
</body>
</html>

ブラウザで読み込むと以下の通りメールアドレスが表示されます。

一方curl等で読み込むと以下となります。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello, World!</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>Contact: <a href="mailto:harunobu@cloudflare.com">harunobu@cloudflare.com</a></p>
</body>
</html>

[ec2-user@ip-172-31-10-151 html]$ curl https://a.harunobukameda.com/
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello, World!</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>Contact: <a href="/cdn-cgi/l/email-protection#234b4251564d4c415663404f4c5647454f4251460d404c4e"><span class="__cf_email__" data-cfemail="f79f96858299989582b7949b988293919b968592d994989a">[email&#160;protected]</span></a></p>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script></body>
</html>

以下の部分がスクリプトに置き換えられブラウザがそのスクリプトを実行することでemailアドレスを画面に表示させています。

    <p>Contact: <a href="/cdn-cgi/l/email-protection#234b4251564d4c415663404f4c5647454f4251460d404c4e"><span class="__cf_email__" data-cfemail="f79f96858299989582b7949b988293919b968592d994989a">[email&#160;protected]</span></a></p>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>

email-decode.min.jsは以下となっており難読化されています。

!function(){"use strict";function e(e){try{if("undefined"==typeof console)return;"error"in console?console.error(e):console.log(e)}catch(e){}}function t(e){return d.innerHTML='<a href="'+e.replace(/"/g,"&quot;")+'"></a>',d.childNodes[0].getAttribute("href")||""}function r(e,t){var r=e.substr(t,2);return parseInt(r,16)}function n(n,c){for(var o="",a=r(n,c),i=c+2;i<n.length;i+=2){var l=r(n,i)^a;o+=String.fromCharCode(l)}try{o=decodeURIComponent(escape(o))}catch(u){e(u)}return t(o)}function c(t){for(var r=t.querySelectorAll("a"),c=0;c<r.length;c++)try{var o=r[c],a=o.href.indexOf(l);a>-1&&(o.href="mailto:"+n(o.href,a+l.length))}catch(i){e(i)}}function o(t){for(var r=t.querySelectorAll(u),c=0;c<r.length;c++)try{var o=r[c],a=o.parentNode,i=o.getAttribute(f);if(i){var l=n(i,0),d=document.createTextNode(l);a.replaceChild(d,o)}}catch(h){e(h)}}function a(t){for(var r=t.querySelectorAll("template"),n=0;n<r.length;n++)try{i(r[n].content)}catch(c){e(c)}}function i(t){try{c(t),o(t),a(t)}catch(r){e(r)}}var l="/cdn-cgi/l/email-protection#",u=".__cf_email__",f="data-cfemail",d=document.createElement("div");i(document),function(){var e=document.currentScript||document.scripts[document.scripts.length-1];e.parentNode.removeChild(e)}()}();

Claudeを使って元に戻してみると完ぺきではないもののおおよそ以下が実行されていることがわかります。

(function() {
    // メールアドレスを復号化する関数
    function decodeEmail(encodedString) {
        let decoded = '';
        const key = parseInt(encodedString.substr(0, 2), 16);
        
        // 2文字ずつ処理
        for (let i = 2; i < encodedString.length; i += 2) {
            const byte = parseInt(encodedString.substr(i, 2), 16) ^ key;
            decoded += String.fromCharCode(byte);
        }
        
        return decoded;
    }

    // ページ内の暗号化されたメールアドレスを探して復号化
    function processEmails() {
        const elements = document.querySelectorAll('.__cf_email__');
        elements.forEach(element => {
            const encoded = element.getAttribute('data-cfemail');
            if (encoded) {
                const decoded = decodeEmail(encoded);
                element.replaceWith(decoded);
            }
        });
    }

    // 実行
    processEmails();
})();

注意点

CloudflareはデフォルトではHTMLはキャッシュしませんが、この機能はHTMLに対して動作します。

つまり毎回動的にCloudflare側でHTMLを書き換えています。
私の検証環境ではこの機能がオンになると30msecほど読み込みが遅延しています。この点は注意が必要です。

Discussion