種別[software] cocolog:68180670
セクションJRF のソフトウェア Tips
日時2011年05月07日 22:32:13
元URLhttp://jrf.cocolog-nifty.com/software/2011/05/post.html
タグ[セキュリティ] [JavaScript]

電子署名の替わりに Loaded Magic

今、私は「ひとこと」を書くためのウェブ上のエディタを MovableType、SSI、 Javascript を使って作っている。追加的な機能として「ひとこと」に電子署名のようなものを付けて、その偽造がないことを証明できたらカッコイイだろうなぁと考えた。

しかし、ウェブの標準的で本格的な電子署名技術を、上記の枠組みだけから使うことは不可能だろうし、例えば、コメントのハッシュを付すにしても、そのハッシュの偽造もコメントの偽造とほぼ同程度の容易さと考えるべきだから、逆に変な信用を与えて、話をややこしくしてもいけない。

この「セキュリティ」を考慮する以前の段階で、まず、コメントの一部削除をするとき、自分が開いた別のページのデータが先に反映したのを忘れて、意図しないコメントの削除が行われないように、コメントにマジックナンバーを付け、そのマジックナンバーが合っているかどうかをチェックするという、簡単な安全策を取っていた。

このマジックナンバーのテクニックは、昔のウェブ掲示板などではよく使われるテクニックだったと思う。複数のコメントのマジックナンバーが重ならなければよいので、私が使うマジックナンバーは四ケタの数字を乱数的に生成したものでしかない。

これをコメントのハッシュの最初の四ケタなどとする実装も可能だが、それは上記のように電子署名としての意味をもたず、変な誤解を与えかねない。

でも、何かできないかなぁ…と考えているうちにヒラめいた。

<b>「悪者」が、ページの送信途中に人知れず偽造を行える環境なら、「正義の味方」も、ページの送信途中に人知れず細工できるだろう。その可能性があることが、「悪者」を牽制できるのではないか?</b>

具体的には下のようなマジックナンバーをランダムに返す関数をわざわざロードしてから使うのである。(summoner.js がその現実のプログラム。)

function allot_magic(text) {
    return Math.random().toFixed(5).substr(2,4);
}

「正義の味方」はこのプログラムを送信途中に次のようなプログラムに差し替える。

var SALT = "ABCD";
var UPLOAD_TEXT_JS = "https://your.server/tools/upload_text.js&quot;;
var CLIENT_PERMIT = "XYZ12345";
var pending_texts = new Array();
var pending_timer = null;

function allot_magic(text) {
    chrsz = 16;
    var hash = dec_sha1(SALT + text);
    pending_texts.push([text, hash]);
    if (! pending_timer) {
      pending_timer = setTimeout(upload_pending_texts, 500);
    }
    return hash.substr(0,4);
}

function upload_pending_texts(res) {
    if (pending_texts.length) {
      var p = pending_texts.shift();
      var text = p[0];
      var hash = p[1];
      upload_text(text, hash, upload_pending_texts);
    } else {
      pending_timer = null;
    }
}

function dec_sha1(s) {
    var binarray = core_sha1(str2binb(s), s.length * chrsz);
    var r = "";
    for (i = 0; i < binarray.length; i++) {
      var n = binarray[i].toString(10);
      r += n.substring(n.length - 1, n.length);
    }
    return r;
}

var scr;
scr = document.createElement("script");
scr.src = 'http://oauth.googlecode.com/svn/code/javascript/sha1.js';
document.body.appendChild(scr);

scr = document.createElement("script");
scr.src = UPLOAD_TEXT_JS + "?permit=" + encodeURI(CLIENT_PERMIT);
document.body.appendChild(scr);

上は最後で sha1.js と「正義の味方」が管理するセキュアなサーバーのディレクトリ(上の例では https://your.server/tools/)に置いた次の upload_text.js を読み込む。

var LOG_SERVER = "https://your.server/log_text_database.cgi&quot;;

function upload_text(text, hash, callback) {
    var data = "text=" + encodeURI(text) + "&hash=" + encodeURI(hash)
      + "&uri=" + encodeURI(location.href);
    var httpobj = requestFile(data, "POST", LOG_SERVER, true, callback);
}

function createHttpRequest() {
    if(window["ActiveXObject"]){
      try {
        return new ActiveXObject("Msxml2.XMLHTTP");
      } catch (e) {
        try {
          return new ActiveXObject("Microsoft.XMLHTTP");
        } catch (e2) {
          return null;
        }
      }
    } else if(window["XMLHttpRequest"]){
      return new XMLHttpRequest();
    } else {
      return null;
    }
}

function requestFile(data, method, fileName, async, on_loaded) {
    var httpoj = createHttpRequest();
    
    httpoj.open(method, fileName, async);
    httpoj.onreadystatechange = function() { 
      if (httpoj.readyState==4 && on_loaded) { 
        on_loaded(httpoj);
      }
    }
    if (data) {
      httpoj.setRequestHeader("Content-Type",
                              "application/x-www-form-urlencoded");
    }
    httpoj.send(data);
    
    return httpoj;
}

すると、「悪者」が知らずに勝手にマジックナンバーを割り振ったものを見せて、誰かにユーザーの別の印象を与えているとき、「正義の味方」は、それがユーザーの意図ではないという証拠をネット上に保持し、場合によってはユーザーも「悪者」も知らないところで対抗することができる。

もちろん、「悪者」が summoner.js を狙ってくることもあるだろう。しかし、国家や信託銀行など合法的な「正義の味方」はブログのプロバイダを味方に付けられるのだから、最終的に有利なのは「正義の味方」だろう。

……とかなり妄想が入ったことを考えた。セキュリティ的には、「正義の味方」であれ、こういうことあっちゃならんのだろうけど。

(ちなみに、スクリプトを分けたのは、XMLHttpRequest のクロスドメイン制限を避けるため。ユーザーが何者かは https の Cookie などで確かめているとすべきで、SALT はもちろん、CLIENT_PERMIT もセッションごとに変えられるとしても、それはサーバーへの DOS 攻撃を少しだけ難しくするぐらいの意味しかない。あと、sha1.js は外部のサーバーから "include" するのではなく、ライセンスをクリアにした上で、ソースにベタ書きするなり、自分のサーバーに置くなりすべきだし、一般に、関数名が重ならないよう考慮する必要もある。)

(えーっと、私はセキュリティどころかプログラムの専門家ではないことに注意!専門家から見れば、ツっこみどころはいろいろあると思う。例えば、ハッシュの使い方として、SALT なんて今は使わないし、dec_sha1 はかなり反則だと思う。この記事の最初のバージョンではクロスドメイン制限を忘れていたぐらいだし。)

■参考
  ●《aboutme2cocolog:アバウトミーの「ひとこと」をココログプロへ移転するツール》。最初に書いた今私が作ってる「ひとこと」を書くためのウェブ上のエディタ。
  ●《Ajaxはじめの一歩 XMLHttpRequest - [Javascript] All About》。上のソースの XMLHttpRequest あたりの元。
  ●《sha1.js - oauth - API needz authorized? - Google Project Hosting》。他の OAuth を使う記事からこの SHA-1 のソースを見つけた。上で "include" して使っている。
    
更新:2011-05-07,2011-05-09
初公開:2011年05月07日 22:32:11
最新版:2011年05月09日 05:29:37


Comments:

[E:pencil] 更新:ほんのちょっとしたところ。送信するデータに location.href  も渡すようにしておいた。Cookie とかあればいいんで、逆にこれがあることで余計な判断が増えるかもしれないとは思ったが、いちおう。
投稿: JRF | 2011-05-08 03:12:49 (JST)

[E:pen] 更新:LOG_SERVER のところで XMLHttpRequest のクロスドメイン制限への注意を喚起した。実際には、upload_text 部分は別のファイルにし、別のセキュアなサーバーから、ソースの最後でやってるように script タグを書き込んで (つまり "include" して)使えばクロスドメインで通信できるようになる(と思う)。

逆に、上で script タグで書き込んでいる sha1.js については、セキュアサーバー上にコピーしてから読み込んだり、ライセンス関係をクリアにした上で、ソースの中にベタ書きしたほうがいい。(上で私が書いたソースのライセンスに関してはもちろん Public Domain、数式的利用の範囲ってことでいいです。)

投稿: JRF | 2011-05-08 21:04:06 (JST)

[E:clip] 更新:一つ前のコメントの検討結果を、記事に反映させ、「正義の味方」が差し替えたソースが一つだったものを二つに分けた。ついでに、ソースを archive に置きリンクも張っておいた。……より正確にはなったかもしれないが、わかりにくくなったと思う。可能性を酌んでくれる人が増えるならうれしいが、変に私の書き方が信頼されやすくなったとすれば失敗。

投稿: JRF | 2011-05-09 03:47:12 (JST)

[E:sleepy] 更新:pending_timer のチェックの処理を入れる。(入れたあとちょっと修正もする。)最初の投稿の前の段階で入っていたチェックだったが、何を血迷ったか抜いてしまっていた。javascript の thread まわりのロックとかどうなっているのか自信がないので、血迷った感じ。リーダライタロックとかどう書くんだろう?

投稿: JRF | 2011-05-09 05:32:43 (JST)

[E:pouch] 更新:記事は変えてない。リンクしてあるソースの中を、変数名の衝突が起きにくいように隠蔽化(カプセル化?)した。でも、これで十分なのか自信はない。これ以上ややこしくしても読みにくくなるだけだから、記事のほうには反映しなかった。

投稿: JRF | 2011-05-10 04:10:39 (JST)

[E:cd] 更新:記事は変えてない。上に引き続きリンクしてあるソースを頻繁に変えていることを報告しようと思ってコメント。横から関数を乗っ取ることへの防御を考えてた。どこまでうまくいっているか不明。今のバージョンだと関数の caller 属性も使っているが、これは今後使っちゃダメって話も見たし、なんか私に根本的な誤解がありそうな雰囲気。

投稿: JRF | 2011-05-10 18:02:44 (JST)

[E:memo] 更新:…というか、隠蔽化に関してちょっと検討してみた。inter-script delegation みたいに言えばいいのかな。それがどうもムリそうなので、caller をチェックするルーチンを入れていたりしていたが、新しいブラウザにはある defineProperty を使えば、ラクに安全を確保できそうに思う。まぁ、ただ私がそう思うだけで、s が付いてない http で CLIENT_KEY を渡したりするんだから、専門家から見れば安全じゃあありえないんだろうけど。

投稿: JRF | 2011-05-13 12:25:55 (JST)

[E:down] 更新:記事は変えてない。リンクしてあるソースで、aboutme2cocolog の statuses_editor.user.js のからみで、USER_NAME も updater_hint としてサーバーに送ることにした。あくまでヒント。

投稿: JRF | 2011-05-19 05:19:19 (JST)

後方参照 (9 件)