WordPressのインラインJavaScriptをなくして、Content Security Policyを設定

うちのテーマは今のこのデザインになるまでは、サードパーティーのテーマを使っていたんです。
で、そのテーマには脆弱性があってXSSが有効だったんですよー((゜Д゜;))

今は1から作った独自のテーマを使っていて、汎用的な拡張性(テーマに必要な画像等のアップロード機能やAJAX)や外部を参照する不明なコードはありません。
なのでテーマ経由の脆弱性はない感じなのです。

だけれど、XSSを防ぐセキュリティ関連のヘッダーを設定しておいた方が良いって事なのでApacheにヘッダーを追加してみました。
新しく追加したのはX-Content-Type-Options、X-XSS-Protection、Content-Security-Policyの3つで、どれもXSSを防ぐのに効果が高いもの。
X-Frame-Optionsだけはブログをiframeで表示されていたサイトがあったので設定済みでしたw

設定内容はこんな感じ。

Header always set X-Frame-Options SAMEORIGIN
Header always set X-Content-Type-Options nosniff
Header always set X-XSS-Protection "1; mode=block"
Header always set Content-Security-Policy "default-src 'self' *.wolfs.jp; script-src 'self' *.wolfs.jp; child-src 'self' data: www.youtube.com; style-src 'self' 'unsafe-inline' *.wolfs.jp; img-src 'self' data: *.wolfs.jp *.gravatar.com;"

 
で、今回躓いたのはContent Security Policy(CSP)の設定。

JavaScriptやCSSは以前から1つにまとめているので大した問題ではなかったのですが、CSPで「script-src 'self' *.wolfs.jp」のようにインラインスクリプトを許可しない設定だとWordPressが出力するコメント関連のスクリプトが動かなくなっちゃう。
かと言って許可するとXSSに対して弱くなり、CSPを設定しないのとあまり変わらないような結果になります。

うちの環境で見た感じ、主にWordPressが出力するインラインJavaScriptは、
 ・コメントフォームの隠しフィールド「_wp_unfiltered_html_comment_」
 ・コメントのある「返信」ボタンのonclick
のようです。

とりあえず、その辺を無効化して無効化した物を再度使用可能にするように作っていきます。
続きを読む

archive.isにアーカイブされたページで警告を表示する。

archive.isをブロックしたりして対策をしてもアーカイブされちゃう事があります。
その対策の間をぬってアーカイブされちゃったページに警告を表示するJavaScriptを作ってみました。

if (location.hostname != "yourdomain") {
    document.title = "[不正コピー]";
    document.getElementsByTagName("body")[0].innerHTML = "";
 
    var i, html = "";
    for (i = 0; i <3000; ++i) {
        html += "/// 不正コピーです! /// ";
    };
    document.getElementsByTagName("body")[0].innerHTML = html;
    html = null;
};

コードは簡単、location.hostnameを比較してyourdomain以外なら警告を表示するコードを実行するだけ。
動作内容はタイトルを「[不正コピー]」に変更し、BODYエレメント内容を全部削除し代わりに「/// 不正コピーです! ///」の文字を3000回並べます。
yourdomainはあなたのサイトのドメイン名に変更して下さい、変更しないと本物のサイトでこのメッセージが表示されて悲惨なことになりますw

JavaScriptなので当然JavaScriptを動作しないようにしてある環境では動きませんが、現在はJavaScriptが動かない環境の方が少ないのである程度の効果はあると思います。

序で・・と言ったらあれですが・・
この文字を表示する動作を重くしたい場合は以下のようにコードを変更するだけ。

if (location.hostname != "yourdomain") {
    document.title = "[不正コピー]";
    document.getElementsByTagName("body")[0].innerHTML = "";
 
    for (var i = 0; i <3000; ++i) {
        document.getElementsByTagName("body")[0].innerHTML += "/// 不正コピーです! /// ";
    };
};

これは文字が追加される毎にDOMのレンダリングが発生するのでレンダリング負荷が増えます。
低スペックのPCやモバイル端末ではクラッシュさせる事もできるかも。
本当に重くさせるのであれば無限ループ化すればOK。

ちょっと応用すればarchive.isのロボットを狙い撃ちでクラッシュさせアーカイブを中断させる事も可能です。

Twitterにアップされている動画のTSファイル一覧を表示&保存する

Twitterにアップされている動画で保存したい物があったので、HTTPリクエストログからURLをゲットしようとおもったら・・・
なんとm3u8の再生リスト形式になっていて動画自体も.tsに分割されていました。

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=320000,RESOLUTION=240x180,CODECS="mp4a.40.2,avc1.42001f"
/ext_tw_video/.../pu/pl/240x180/???.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=832000,RESOLUTION=480x360,CODECS="mp4a.40.2,avc1.42001f"
/ext_tw_video/.../pu/pl/480x360/???.m3u8
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:3
#EXTINF:3.000,
/ext_tw_video/.../pu/vid/0/3000/480x360/???.ts
#EXTINF:3.000,
/ext_tw_video/.../pu/vid/3000/6000/480x360/???.ts
#EXTINF:3.000,
/ext_tw_video/.../pu/vid/6000/9000/480x360/???.ts
#EXTINF:3.000,
/ext_tw_video/.../pu/vid/9000/12000/480x360/???.ts
#EXTINF:3.000,
/ext_tw_video/.../pu/vid/12000/15000/480x360/???.ts
....

再生リストから欲しいサイズの動画を選んで分割ファイルをダウンロードすれば良いんだろうけど面倒くさいw
それに、分割は3秒毎なので2分を超える動画は70ファイル超え!

そんなの1つずつURLをコピペなんてやってられない・・って事で、動画サイズ毎のURLとファイルを一覧表示するGreasemonkeyスクリプトを作ってみました。
動作してる動画はこちら。

1ファイル毎に手動保存なのはローカルに自動でファイルを保存するAPIが呼び出せないから。
userChrome.jsとして動かせばできるんだけど、セキュリティ的にあまり宜しくない。

まあ随分と楽になるんじゃないかなーと。
保存時にはINDEXに表示されている番号と同じファイル名にしないと、TSファイルの連結時に時系列がごちゃごちゃになりますw

おまけ?として法人など業界用の動画VMAP形式の動画も落とせるようにしました。
分割されていないので楽ちんですw
続きを読む

e621.netで画像保存を楽にする

e621.netってケモノ絵や話題を投稿するサイトがあるんです。
ここは健全な絵もあるけれどR18な絵も多数ある所。
ケモノが好きな人は見てて飽きない所ですねw

ぼーっと見ていたら、とあるアーティストさんの過去絵が沢山あったので、ガンガン保存していたら保存ダイアログを出すのがメンドウになってきましたw
なので自動化・・はダルイしイラナイ画像も拾うから画像をダブルクリックするだけで保存ダイアログが出るようにuserChromeを作ってみました。

e621net-supporter.uc.js

// ==UserScript==
// @name           e621.net: 画像保存サポーター
// @namespace      http://blog.wolfs.jp/
// @description
// ==/UserScript==
 
var e621netSupporterObj = {
    targetHost: 'e621.net',
    targetPage: 'e621.net/post/show/',
 
    isTargetPage: false,
    lastTime: 0,
 
    clickID: {
        left: 0,
        right: 2,
        center: 1
    },
 
    init: function() {
        gBrowser.mPanelContainer.addEventListener("dblclick", this, true);
    },
 
    handleEvent: function(event) {
        if (gBrowser.currentURI.asciiSpec.indexOf(this.targetPage) != -1) {
            event = new XPCNativeWrapper(event);
            if (event.target instanceof HTMLImageElement) {
                if (event.target.id == 'image') {
                    event.preventDefault();
 
                    if (new Date().getTime() - this.lastTime > 5000) {
                        this.lastTime = new Date().getTime();
 
                        var img = this.getImage(event.target.src);
                        if (img != null) {
                            this.saveImage(img, this.getFileName(event.target.src));
                        }
                        img = null;
                    }
                }
            }
        }
    },
 
    getFileName: function(url) {
        return url.match(".+/(.+?)([\?#;].*)?$")[1];
    },
 
    getImage: function(url) {
        var request = new XMLHttpRequest();
        request.open('GET', url, false);
        request.overrideMimeType('text/plain; charset=x-user-defined');
        request.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
        request.send(null);
 
        if (request.status == 200) {
            return request.responseText;
        }
 
        request = null;
        return null;
    },
 
    saveImage: function(imageData, requestFileName) {
        var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(Components.interfaces.nsIFilePicker);
        fp.init(window, 'Select a file', Components.interfaces.nsIFilePicker.modeSave);
        //fp.appendFilters(Components.interfaces.nsIFilePicker.filterImages);
        //fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
        fp.appendFilter('JPEG Image', '*.jpg');
        fp.appendFilter('PNG Image', '*.png');
        fp.appendFilter('GIF Image', '*.gif');
        fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
 
        fp.defaultString = requestFileName;
        switch (requestFileName.split('.')[1]) {
            case 'jpg':
                fp.defaultExtension = 'jpg';
                fp.filterIndex = 0;
                break;
            case 'png':
                fp.defaultExtension = 'png';
                fp.filterIndex = 1;
                break;
            case 'gif':
                fp.defaultExtension = 'gif';
                fp.filterIndex = 2;
                break;
            default:
                fp.filterIndex = 3;
        }
 
        switch (fp.show()) {
            case Components.interfaces.nsIFilePicker.returnOK:
            case Components.interfaces.nsIFilePicker.returnReplace:
                var outStream = Components.classes["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
                outStream.init(fp.file, 0x02 | 0x08 | 0x20, 0664, 0);
                outStream.write(imageData, imageData.length);
                if (outStream instanceof Components.interfaces.nsISafeOutputStream) {
                    outStream.finish();
                } else {
                    outStream.close();
                }
                outStream = null;
                break;
            case Components.interfaces.nsIFilePicker.returnCancel:
                break;
        }
    }
};
 
e621netSupporterObj.init();

本当はグリモンとかでサクッと実装したかったけれど、保存ダイアログをだすfilepickerやfile-output-streamなんかは呼び出せないので、権限があるuserChromeで実装。

内容は簡単。
 1.ダブルクリックされたらページのURLがe621.net/post/show/かどうか比較。
 2.対象のページだった場合はクリック先のエレメントがHTMLImageElementかつIDがimageか確認。
 3.imageだった場合はXMLHttpRequestで画像のバイナリを獲得。
   これは既に表示されている画像のキャッシュが効くのでいいb
 4.画像を正常に読み込めたら保存ダイアログを呼び出して保存先やファイル名を決定。
って感じ。

userChromeだけれど、内容はFirefox系のAPIに依存しているのでFirefox系ブラウザ専用です。

イカロスオンラインをFirefoxから起動する

前回の記事「イカロスオンラインをActiveXなしで起動する」ではuserChrome.js又はGreasemonkeyから起動に必要な引数をゲットするだけででしたが・・・
今回はランチャーの起動まで全部Firefoxから行えるようにアドオンを作ってみました。

とりあえず、どんな感じか?ってのを録画してみました。

ブラウザはCyberfox x64ですがアドオン等の仕様は同じなので普通のFirefoxでも問題ありません。
起動時に右下に出てきているのはセキュリティソフト関連なので無視してください。

特に違和感もなく起動できると思います。
ゲームのインストール場所を変更している場合はabout:configから「extensions.io_weblauncher.launcher」の値を変更してください。

実装に当たって、前回のスクリプトと違う点は公式のJavascriptを乗っ取っていない所。
これはセキュリティ関連の影響かアドオン側からページのDOMを書き換えることが出来なかったので、正規表現で強引に起動コードをゲットしています。
scriptエレメントの構成が変わったら動かなくなる代物ですw

こうすればいいよーって所があれば遠慮なく言ってください。

いやー、最初はNPAPIで実装した方が楽なんじゃないか?って思ってたけれど、FirefoxのアドオンはJavaScriptと同じなので楽々でした。
楽々と言っても色々調べたりするのに2時間ぐらいかかりましたけど・・w

拡張機能のダウンロード
ICARUS ONLINE: WebLauncher v1.0.1 [2015/07/03, 15,170Bytes, e58901b6bb4582d6b47ce074ec0d63a9]

更新履歴

[2015/07/03] v1.0.1
 ・gamecom版イカロスに対応
 ・scriptエレメントの構成が変動しても動作するように変更

[2015/07/02] v1.0.0
 ・ハンゲーム版イカロスに対応

xpi自体はZIPファイルなので不安な方は解凍してソースコードを確認してみてください。