ad4Uの隠しリンクを露出させるGreasemonkeyスクリプト

ソースが一部表示されていなかったようです。今は修正済みです。申し訳ありません。
ライブドア楽天が使用しているドリコム行動ターゲティング広告「ad4U」。
ブラウザに残っているサイト訪問履歴を元に広告を出してくる、プライバシーも何もあったもんじゃないアレです。で、ad4u利用サイトでは、閲覧履歴の取得のために大量の隠しリンクが埋め込まれています*1

さて、この隠しリンクGreasemonkeyスクリプトで表示させる、というのが今回のお話。
IEにおいては、高木先生が作られたユーザースタイルシート隠しリンクを露出させることが出来ます(http://takagi-hiromitsu.jp/diary/20081211.html#p01)。アラートを出すようにしておけば、ad4Uな広告が入っているサイトにすぐに気づけて便利です。
私も「楽天ad4U、個人ブログまで監視対象にしていた」を書くときに利用させていただきました。

しかし、この「スタイルシートを利用する」というアイデアの参考元であるところのamachang曰く「まともな CSS パーサーだとエラーにな」る手法を使っているためでしょうか、Firefoxだとうまくいきません。

私が普段使っているFirefoxでも、ad4Uを使ったサイトにすぐに気づきたいし、隠しリンクも見たい。こういうときは、グリモンだ。ということで、Greasemonkeyスクリプトを書いてみました。
このGreasemonkeyスクリプトは、隠しリンクの露出が出来るだけで、ad4Uな広告そのものをブロックできるわけではありません。ad4Uの無効化は末尾にあげた関連エントリをご覧ください。

// ==UserScript==
// @name           Show ad4U hidden links
// @namespace      http://d.hatena.ne.jp/xenoma/
// @description    Show ad4U hidden links.
// @include        http://*
// @author         xenoma
// ==/UserScript==

// ad4Uによるページ表示履歴解析を完全に無効にするわけではありません
// アラートを出し、履歴解析のターゲットになっているURLを表示するだけです
(function(){
	var bodyElement = document.body;
	var ad4u_detect = function(){
		// 履歴対象のURLリストが入っているdiv要素
		var ad4u_list = document.getElementById('ad4u_list');
		
		if(ad4u_list){				
			bodyElement.removeEventListener('DOMSubtreeModified', ad4u_detect, true);
			
			alert("Hello, ad4U!");
			var source = ad4u_list.innerHTML;
			
			// plaintext で出力するとき
			// var element = document.createElement('plaintext');
			
			// リンク形式で出力するとき
			var element = document.createElement('div');
			source = source.replace(/<a href="(.+?)">[^<]*<\/a>/gi, "<A href=\"$1\"><NOBR>$1<\/NOBR><\/A><BR \/>");
			
			element.style.cssText="overflow:scroll; border:dashed 4px red; width:290px; height:75px;";
			element.innerHTML = source;
			bodyElement.appendChild(element);
		}
	};
	bodyElement.addEventListener('DOMSubtreeModified', ad4u_detect, true);
})();

とりあえずFx3, GM0.8の環境では動いています。隠しリンクは<div id = "ad4u_list">に入っているのですが、元々は存在しておらず、後から追加されるようでした。そこで、DOM Treeの変化を手がかりにすることにしました。
余談ですが、plaintextで出力するようにすると、いくつかのdiv要素に分かれていることがわかります。この区切りはおそらく広告ジャンルです。ジャンルごとにどんなURLが見られているかも見えてきます。ジャンルの詳細は、ドリコムのプレスリリースがありますので以下に上げておきます。

髪に関するURL群は、コスメ>化粧品という扱いかな。

注意

どうぞご利用ください、といいたいところだけど、Greasemonkeyスクリプトをインストールすることはリスクを伴います。悪意あるスクリプトや穴のあるスクリプトを組み込んでしまわないよう、得体の知れないサイトの言うことを信じて安易にインストールしてはいけませんよ*2

追記

ソースの後半が切れていたので、表示されるよう修正。もしかして途中で<が入ると切れちゃうのか?スーパーpre記法@javascript

追記2

使ってみて ad4u の発動条件について気づいたことなど

*1:訪問済みのリンクの色が異なるのを利用している。未訪問のリンクと訪問済みリンクのスタイルに違いをもたせて、その違いを検出している。

*2:それでも構わないなら、どうぞご利用ください

ShareTabsは便利でスクリプトは使えないサイトです

ShareTabsが危ないっていうエントリを書いた後、舞い上がって脆弱な部分を晒しただけの俺は極悪人じゃないかと後悔し、猛反省していた。せめてもの罪滅ぼしとして、連絡先を探してメールした。
その後毎日チェックしてたものだから、サービスがエラーになった時はほっとした。たぶん修正初めたんだろうなあと。
そして今日、修正したってお返事が来た。確認しにいったら、タグが打ち破れないようになっていた。過去に登録した分も修正済みのようだ。
これで晒しがチャラになるとは思わないけど、あれはムダ知識になった、ということで勘弁してください。
複数URLを1つにまとめられるのは便利だと思うので、修正されてよかった。

ShareTabs: http://www.sharetabs.com/

livedoorのad4U、拒否できるのは「1ヶ月」だけ

今は 1 年に延びた模様です(末尾に関連エントリ)。
昨日楽天ad4Uについていろいろ書いたんですが、今回はライブドアのad4Uについて。ライブドアドリコム行動ターゲティング広告「ad4U」を利用しているサイトだ。サイトの訪問履歴を基準に広告を出してくるというのは当然一緒だから、今さら書くことなし。でも、拒否に対する姿勢に違いがあるので、それについて書く。

livedoorのad4Uを拒否する方法

楽天のad4Uを拒否する方法は前回のエントリをご覧ください。ざっくり言えばこんな感じです。

  • 所定のページで拒否する意思を示す
  • (楽天の)ad4Uを無効にするcookieが渡される
  • 有効期限は1年
  • ただし、NGUserIDなる(おそらく重複なしの)識別用IDも同時に渡される

livedoorの場合も所定のページで拒否の意思を示せばOK。下記から設定できる。
株式会社ドリコムの行動ターゲティングの説明とその無効化について - livedoor

ここで「ドリコム行動ターゲティング広告を無効化する」ボタンをクリックすればよい。これで(livedoorの)ad4Uは拒否できる。楽天同様、こちらもcookieを利用しているので、受け入れるようにしておく必要がある。ステータスが「ドリコム行動ターゲティングが 無効になっています。」に変化すれば成功だ。
拒否状態を表すcookieはbta_optoutという名前で、値はtrueという至ってシンプルなもの一つだけだ*1

楽天の場合と違い、IDはふられないのでその点は評価できる。しかし、有効期限が楽天より短い。わずか1ヶ月だ。

有効期限短すぎ

bta_optoutの有効期限はわずか1ヶ月だ。拒否の意思を表明しても1ヶ月で水の泡?いくらなんでも短すぎる。

もしかしたら、期限切れが近づくと有効期限を延長したcookieを発行するのかもしれない。そこで、有効期限を約10秒後に書き換えたcookieを作って確認してみた。



※有効期限を弄ったcookie作成の例。実験に使用したcookieとは異なる。

有効期限が切れる直前、つまり残り有効期間10秒以下の状態で、ad4Uが含まれているlivedoorニュースにアクセスしてみた。しかし書き換えは生じず、当初の期限どおり失効した。この状態で無効化設定ページにアクセスしたところ、しっかりとad4U「有効」である旨表示された。livedoorのad4U拒否期間は自動延長されることはないと考えられるので、月に1度は「拒否表明」しなければならない。
さらに面倒なことに、拒否状態を表すbta_optoutが有効であるとき、無効化設定ページで再度「無効化」をクリックしても有効期限が更新されない。10日後あたりに「さらに1ヶ月延長しよう」ということが出来ないのだ*2。拒否期間は文字通り「1ヶ月だけ」で、途中で更新出来ない仕組みだ。だから、しっかり1ヶ月が過ぎ、ad4Uが有効になったあとで無効化する必要がある。cookieを削除すれば1ヶ月たたなくても「無効化が無効」になるので新しくcookieを得られるが、通常求められる操作とはいえないだろう。

まとめ

livedoorのad4Uを拒否するときのまとめ。

  • 所定のページで拒否する意思を示す
  • (livedoorの)ad4Uを無効にするcookieが渡される
  • 個人を特定するIDは発行されない
  • 有効期限は1ヶ月だけ
  • 期限を延ばしたければ、きっかり1ヵ月後に再び同じ操作を行う
    • 1ヶ月待てなければcookie削除後、再び同じ操作を行う

無効化をいかに阻止するか、という設計であると思います。

それにしても、昔はcookieを全拒否していれば安心だったのに、今は場合によってはcookieを受け入れなければマズいんだよなあ。面倒なことになったものです。

*1:docs_sidはセション終了時に廃棄されるので無視

*2:楽天のad4U無効化ページは、「有効→無効」だけではなく「無効→有効」も兼ねているので、「無効→有効→無効」とすればその時点から1年間無効に出来る。

楽天ad4U、個人ブログまで監視対象にしていた

高木先生の記事、「楽天ad4Uの隠しリンクを露出させるユーザスタイルシート」を見ました。モノはためしと、普段使ってないIE6で試してみました。当然問題なく動きました。

こうして列挙されると、どんなサイトの履歴を対象にしてるのか興味が沸きます。が、イチイチカーソル合わせてURLを確認していたら日が暮れてしまいます。タグ解釈されなきゃいいんだから、ということで、次のように書き換えました。divをplaintextにしただけです。

#ad4u_list {
    display: expression(function() {
        if (!this.__mark) {
            this.__mark = true;
//            alert(this.innerHTML);
            var o = '<plaintext style="overflow:scroll; border:dashed 4px red;">';
            o = o + this.innerHTML.replace(/A>/g, "A> ");
            o = o + '</plaintext>';
            document.body.innerHTML = o;
        }   
        return ''; 
    }.apply(this));
}


実行したところ、期待通りソース状態で出てきました。

これをコピペしてゴニョゴニョして得たURLは3722個3706個(空行をカウントしていた)

ちらっと見てみたのだけど、商業的なサイトが入ってるのは当然のこと、検索キーワードや個人ブログなんかもターゲットにされている模様です。んー。
さて、hatena.ne.jpを含むURLは4つありました。はてなキーワード2個、はてなダイアリー2個です。
キーワード分についてあげると、以下の2つです。

はてダ分については詳細を公開するのがちょっとアレな感じなので書きませんが、いずれも個別の記事を対象にしていました。

「趣味で書いてる個人サイトの訪問履歴を勝手に商売に使うってどうなのよ?サイト所持者に(たぶん)連絡してないよね?」と思いました。

追記

Livedoorの広告も検索サイトのクエリを含むURLをターゲットにしているっぽい。リンク数8474個って。

追記2

楽天ad4Uを拒否する方法について書きました
楽天ad4Uの無効化 - xenoma日記

楽天ad4Uの無効化

ブクマコメントで知ったのだけど、楽天ad4Uによるターゲッティング広告を拒否したい場合には下記から設定できるようだ。
【楽天】行動ターゲティングサービスの説明とその無効化について


ここで「無効にする」をクリックすればOK。cookieを使用しているので、許可する必要ありです。さっき試したら、2つほどcookie食わされました。



有効期限は1年のよう。BTA002は中身が空だからどうでもいいとして、NGUserIDは個人特定用のIDっぽくて嫌な感じ。NGUserIDについて、コメントをいただきました。ad4U特有のものではないようです。cookieを消して再取得するたびに値が変わるので、たぶん重複しないものだろう。いい気分じゃないけど、仕方ないか。

はまちちゃん先生のはしーさーふ、ってヤツなんですね。

はまちちゃん先生のはしーさーふ、ってヤツなんですね。

今回のお話とは全然関係ないじゃん!全然クロスってないもん!一人相撲ですね><
ShareTabsだって、よくある短縮URLサービスと対して変わんない。JavaScriptが実行できたって、攻撃先がなかったらなんてことはないですね。TinyURLで作られたURL踏んだらブラクラサイトでした><ぐらいのもんですね。
短縮URLを踏んだら超自動的におかしな振る舞いをします!、っていう点ではみんな仲間。ShareTabsだけを悪く言うのは良くありませんね。
今回の件では、短縮URLはみんな危険です!って思うことにします。

短縮URLを展開するグリモン書いたよー

バグの修正について、末尾に追記しました。

短縮URLってなかなか怖いですね。

字数制限のキツいサイトでURLを貼るとき、よく使われる短縮URLtinyURLとか、RubyURLとか。twitterなんかでよく見ますね。たいてい素敵なサイトの紹介ですので、ホイホイ踏んじゃいます。そのノリで、怪しいサイトでもクセで踏んじゃいがちです。転送先がsuper-burakura.com*1なんてどう見てもデンジャラスなサイトかもしれないのにです!

転送先のURLを事前に確認する方法はないの?

API叩けばいいとかいうけど、たくさんある短縮URLサービスのリファレンスなんて見てらんないんです!他にもいっぱいあるでしょ、短縮URLサービスなんて。ブラウザでURLクリックすればリダイレクトされる、って機能は一緒なのに、なんで面倒なことしなきゃいけないの!俺は面倒なことが嫌いなんだ!
リダイレクト教えてくれるアドオンとかないの?とか思ったんだけど、見つけられませんでした。その代わり、こんなグリモンをみつけました(http://d.hatena.ne.jp/Constellation/20080613/1213374738)。リダイレクト先のURLをゲットできるんですね。じゃあ簡単そうだ。
俺も俺なりに書いてみたよ。

// ==UserScript==
// @name           Expand short URL
// @namespace      http://d.hatena.ne.jp/xenoma/
// @description    show tooltip onshort URL and replace href and HTML
// @include        http://*
// @include        https://*
// @author         xenoma
// ==/UserScript==
(function(){
    
    var FLAG = {
        'replaceTiming': 0,    // 0:リンククリック時 1:読み込み時
        'replaceType': 0    // 0:短縮URLを残す 1:短縮URL自体を置換
    };
    
    //短縮URLサービスのドメインをどんどん追加しよう
      var LIST = [
        /* tinyURL */
        'tinyurl\\.com',
        /* mooo.jp */
        'mooo\\.jp',
        /* mo-v.jp */
        'mo-v\\.jp',
        /* jpan.jp */
        'jpan\\.jp',
        /* snipurl.com */
        'snipurl\\.com',
        /* qqa.jp */
        'qqa\\.jp',
        /* www.qurl.com */
        'qurl\\.com',
        /* rubyURL */
        'rubyurl\\.com',
        /* PURL */
        'zz\\.tc',
        /*is.gd */
        'is\\.gd',
      ];
    var r =LIST.join("|");
    var rx = new RegExp('^https?:\\/\\/([\\w]+\\.)*(' + r + ')');

    // 短縮URL探します
    var ExpandShortUrl = function(){
        var links = document.evaluate('//a', document, null, 
            XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
        for(var i = 0; i < links.snapshotLength; i++){
            var link = links.snapshotItem(i);
            if(link.href && rx.test(link)){
                switch(FLAG['replaceTiming']){
                case 0: // クリック時置換
                    (function(){
                        var c = checkLink.cloneNode(true);
                        var l = (function(_l){return _l;})(link);
                        c.addEventListener('click', 
                            function(){
                                c.removeEventListener('click', arguments.callee,false);
                                c.innerHTML = 'Wait...';
                                getUrl(l, c);
                            },
                            false);
                        link.parentNode.insertBefore( c, l);
                    })();
                    break;
                case 1: // 即時置換
                    var l = (function(_l){return _l;})(link);
                    getUrl(l, null);
                    break;
                }
            }
        }
    }
    
    // CHECK 雛形
    var checkLink = document.createElement('a');
    checkLink.style.cssText="background : rgb(187, 0, 0) none repeat scroll 0% 0%;"
        + "color : rgb(255, 255, 255);"
        + "margin-right: 2px;";
    checkLink.href = "javascript:void(0)";
    checkLink.innerHTML = 'CHECK';
    
    
    // 短縮前URLゲット 
    var getUrl = function(link, cLink){
        var opt = {
            method:'get',
            url:link.href,
            link:link,
            onload:function({finalUrl:url}){
                if(cLink){
                    switch(FLAG['replaceType']){
                    case 0:
                        replaceUrl(cLink, url)
                        break;
                    case 1:
                        cLink.style.cssText="display:none;";
                        replaceUrl(link, url);
                        break;
                    }
                }else{
                    replaceUrl(link, url);
                }
            }
        }
        setTimeout(GM_xmlhttpRequest, 0, opt)
    }
    
    // 書き換え
    var replaceUrl = function(element, url){
        element.setAttribute('title', url);
        element.setAttribute('href', url);
        element.innerHTML = url;
    }
    
    ExpandShortUrl();

})();

入れるとどうなるの

入れる前

入れた後

CHECKをクリックしたら

短縮URLサービスを見つけたら、LISTに追加すればいいだけなので、簡単です。ドメインに含まれる"."を"\\."って置き換えるのがちょっと面倒だけど、それだけです。
ソースの上手な書き方とか、パフォーマンスとかよくわかんないけど、いい感じに動いたよ!Greasemonkey大好き!
使いたいとか直したいとかいう方がいらっしゃいましたら、どうぞご利用ください。

追記

http://hoge.zz.tc/fugaのようにサブドメインが入ったときに動かないバグがありました。40行目を

    var rx = new RegExp('^https?:\/\/([\w]+\.)*(' + r + ')');

から

    var rx = new RegExp('^https?:\\/\\/([\\w]+\\.)*(' + r + ')');

と修正しました。現在表示されているコードは修正済みです。

*1:架空のドメインだよ