岩本隆史の日記帳(アーカイブ)

はてなダイアリーのサービス終了をうけて移行したものです。更新はしません。

Ruby製Webアプリでの入力チェック手順

今回はRuby製Webアプリでの入力チェック手順について考えてみる。Ruby1.9を使い、Railsなどのフレームワークは使わない前提だ。

文字エンコーディングの妥当性検証こそが重要

徳丸浩さんの『体系的に学ぶ 安全なWebアプリケーションの作り方』には「入力処理」として下記3つの処理が挙げられている。

(a)文字エンコーディングの妥当性検証
(b)文字エンコーディングの変換(必要な場合のみ)
(c)パラメータ文字列の妥当性検証

このうち(c)は単なる書式チェック(文字種や長さなどのチェック)なので、アプリの要件にしたがって粛々と行えばよいだろう。

(b)も必要な場合にだけ行えばよいので、さほど重要ではない。

重要なのは(a)で、これをいかにRubyで実現するかだ。同書では(おそらく意図的に)Rubyへの言及はなかった。

Rubyでは(b)→(a)→(c)

RubyのStringには、String#valid_encoding? というメソッドがある。これが(a)に使えるはずだ。ただ、このメソッドにはチェックに使うエンコーディングが渡せない。文字列自体のエンコーディングがチェックに使われる。

前回の記事ではRackの挙動に触れたが、Rackを使おうと使うまいと、入力文字列のエンコーディングがWebアプリ側の求めるものと同一である保証はない。よって String#valid_encoding? の前に(b)を行う必要がある。

(b)には String#encode や Encoding::Converter#convert が使える。結論として、Rubyでの入力チェック例は下記のようになる。

# (b)文字エンコーディングの変換
s = input_string.encode(Encoding::UTF_8)
# (a)文字エンコーディングの妥当性検証
raise 'Bad Request' unless s.valid_encoding?
# (c)パラメータ文字列の妥当性検証
...

UTF-8を選んだのはあくまで例で、他のエンコーディングでも処理順は変わらない。

追記(2011-07-12)

状況によっては、String#encode や Encoding::Converter#convert ではなく String#force_encoding が適切な場合もあるだろう。バイトの並びは変えずにエンコーディング情報だけを変えたい場合だ。そのとき、Rubyでの入力チェック例は下記のようになる。

# (b)文字エンコーディングの変換
input_string.force_encoding(Encoding::UTF_8)
# (a)文字エンコーディングの妥当性検証
raise 'Bad Request' unless input_string.valid_encoding?
# (c)パラメータ文字列の妥当性検証
...

String#valid_encoding? は信頼できるか

気になるのは String#valid_encoding? の信頼性だ。UTF-8についてざっと確認したかぎりでは、malformedなバイト列、冗長表現、U+110000以降、および、サロゲートがfalseとなった。ここまで対応できているならば信頼してもいいだろう。万一バグが見つかったとしてもレポートすればいいだけだ。

現在のほとんどのOSやライブラリ,フレームワークなどのミドルウェアでは,このような冗長なUTF-8表現は禁止されていると考えられます。そのため,冗長なUTF-8による検査の漏れを防ぐもっとも最善の方法は,UTF-8の検査や他の符号化形式への変換をライブラリやフレームワークに任せ,「自前でUTF-8を処理しない」ということに尽きます。

本当は怖い文字コードの話:第4回 UTF-8の冗長なエンコード|gihyo.jp … 技術評論社

Rack::Request#[]でUTF-8文字列が返ってくる件

$ ruby -v
ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-linux]
$ gem list | grep rack
rack (1.3.0)

という環境で:

require 'rack'

class MyApp
  def call(env)
    [200, {"Content-Type" => "text/plain"}, [Rack::Request.new(env)['hoge'].encoding.to_s]]
  end
end

Rack::Handler::WEBrick.run(MyApp.new, :Port => 3000)

を実行し、http://localhost:3000/?hoge=%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A (クエリストリングは「あいうえお」をUTF-8エンコードしたもの)にアクセスしたら「UTF-8」と表示された。

「あれ、自動判別してるのか」と思い、http://localhost:3000/?hoge=%A4%A2%A4%A4%A4%A6%A4%A8%A4%AA (同じくEUC-JPでエンコード)にアクセスしても、やっぱり「UTF-8」。ためしに「Encoding.default_external = 'EUC-JP'」としても変わらなかった。

Rack::Utils#unescape が原因

解せないので Rack::Request のコードを追いかけたところ、Rack::Utils#unescape が原因と分かった。

Rack::Utils#unescape では、下記のように、URI.decode_www_form_component が第2引数なしで呼ばれている。

def unescape(s)
  URI.decode_www_form_component(s)
end

このように第2引数を省略すると、デフォルトの「Encoding::UTF_8」がデコードに使われ、デコードされた文字列のエンコーディングも「Encoding::UTF_8」と設定される。これが原因。

これ以上は追いかけていないが、この挙動は意図せざるものだと思う。思いたい。

Rack::Request#[] の戻り値のエンコーディングを仮定してはならない

挙動がどうであれ、Rackアプリケーションの作者は、Rack::Request#[] の戻り値のエンコーディングを仮定してはならない。現時点では仕様が明文化されていないからだ。

仮定せず、アプリケーション側で扱いたいエンコーディングに変換し、妥当性をチェックする必要がある。この辺は次の日記に書くつもり。

Re: RESTとCSRF

ご無沙汰しております。artonさんの「RESTとCSRF」を読んで僕なりに考えたことを書きます。

お題:

  • 所定の遷移に従っていないクライアントには破壊的な操作をさせたくない
  • 破壊許可識別子をリクエストボディに含めたくない

artonさんの解は下記の通り「破壊許可識別子をURIに含める方式」でした。

いきなり、POST/PUT/PATCH/DELETEできるというAPIでは無く、最初は必ずクライアントからのGETで始まるとする。そして各操作をeditリレーションのリンクで示す。たとえばGETするのが/book/{id}なら、破壊メソッドのURIは、/book/{id}/{key} (keyが破壊許可の識別子)とかすれば良い

RESTとCSRF

僕も異論はありません。が、別解が浮かんだので記しておきます。破壊許可識別子をcookieに保存する方式です。

この方式であれば、artonさんが書かれている「でも、これってセッションIDをパスに埋め込むのと変わらないか」という点が気にならなくなります。URI方式よりステートフル性が増すわけでもありません。

どんなもんでしょうかね。

Twitter botのプログラムを晒してみる

どんなbot

カラオケって、歌いたい曲が配信されてなかったりしますよね。

そのモヤモヤを救済するために、JOYSOUNDという超最高なカラオケ配信サービスが、配信リクエストを受け付けてくれています。リアルタイムリクエストです。投票で毎月200位までに入った曲が(なんらかの問題がないかぎり)配信されます。

投票にはポイントが必要で、このポイントを無駄にしないためには、なるべく200位ギリギリで通過させる戦略が求められます(ちょっと大げさか)。戦略を考えるにあたっては、順位の移り変わりをチェックしておくのが基本となります。

ぼくはキリンジというバンドのファンで、同じくキリンジファンの方々とTwitterで交流しているので、キリンジの曲の順位をチェックしてつぶやくTwitter botがあると便利だと思いました。実物はこれです。

事前準備

botのプログラムを書く前に、bot用のTwitterアカウントを用意したり、投稿に必要な各種キーを取得したり、といった事前準備が必要でした。下記ページの「OAtuhによるログイン方法」を参考にしました。

環境

早く公開したかったので、ぼくなりに早く書けるRubyを選びました。バージョンは 1.9.2-p136 です。

プログラム

美しくて堅牢なプログラムを書こうなどとは最初から思わなかったので、ざっと書いたら、こんな感じになりました。特に解説はしません。

# encoding: utf-8

require 'nokogiri'
require 'open-uri'
require 'twitter'
require 'escape_utils'
require 'escape_utils/url/rack'  # Rack::Utils#escapeのwarningを防ぐ

def utasuki_doc(singer)
  url = 'http://joysound.com/ex/utasuki/request/search.htm' +
        '?searchKbnGet=2' +
        "&searchKeyWordGet=#{EscapeUtils.escape_url(singer)}" +
        '&sortkey=8'
  Nokogiri::XML(open(url))
end

def songs(doc)
  xpath = "//tr[@class='odd' or @class='even' or @class='evenlast']"
  doc.xpath(xpath).map do |row|
    {rank:  row.xpath('./td[2]').text,
     title: row.xpath('./td[3]').text,
     votes: row.xpath('./td[6]').text}
  end
end

def songs_chunk(songs, hashtag)
  [] if songs.size == 0
  songs_chunk = [[]]
  songs.each do |song|
    if status(songs_chunk.last + [song], hashtag).size <= 140
      songs_chunk.last.push(song)
    else
      songs_chunk.push([song])
    end
  end
  songs_chunk
end

def statuses(songs, hashtag)
  songs_chunk(songs, hashtag).map {|ss| status(ss, hashtag)}
end

def status(songs, hashtag)
  songs_text = songs.map {|s| song_text(s)}.join('')
  "【うたスキ】#{songs_text} ##{hashtag} #{Time.now.strftime('%H:%M')}"
end

def song_text(song)
  "#{song[:rank]}位「#{song[:title]}#{song[:votes]}"
end

def tweet(statuses)
  Twitter.configure do |config|
    config.consumer_key = '{consumer_key}'
    config.consumer_secret = '{consumer_secret}'
    config.oauth_token = '{oauth_token}'
    config.oauth_token_secret = '{oauth_token_secret}'
  end
  statuses.each do |status|
    Twitter.update(status)
  end
end

doc = utasuki_doc('キリンジ')
songs = songs(doc)
abort 'no songs' if songs.size == 0
statuses = statuses(songs, 'kiririku')
tweet(statuses)

puts 'done.'

下記のライブラリを使っています。

  • twitter (1.1.1)
  • nokogiri (1.4.4)
  • escape_utils (0.1.9)

定期実行

上記のプログラムを /home/iwamot/projects/kirikara/bot.rb に保存し、crontabで毎時5分に実行しています。設定は下記の通りです。

$ crontab -l
GEM_HOME=/home/iwamot/.rvm/gems/ruby-1.9.2-p136
5 * * * * /home/iwamot/.rvm/rubies/ruby-1.9.2-p136/bin/ruby /home/iwamot/projects/kirikara/bot.rb

『プログラマが知るべき97のこと』を献本いただいた

プログラマが知るべき97のこと

プログラマが知るべき97のこと

普通に買おうと思っていたんですが、査読に参加させていただいた関係で、オライリー・ジャパン様よりご献本いただきました。同社ならびに監修者の和田さん、ありがとうございます。

プログラミングに関する数ページのエッセイがぎっしり詰まった本です。

ぼくは査読の段階から、まつもとさんの書きおろし「名前重要」に衝撃を受けていました。なるほどと頷ける話であり、すぐに実践したくなる内容です。

ぼくの個人的な「良書」の定義は「生き方や習慣が変わる本」です。その意味において本書は良書でした。未読のエッセイもたくさんあるので、読み進めるのが楽しみです。

PHP-users.jpのコンテンツを更新した

これですこれ。

覚えてますか。

PHP-users.jpのコンテンツを更新する方法」という記事に書いたとおり、PHP-users.jpのコンテンツはGitHubで管理されています。

そのリポジトリに対し、hidenorigotoさんから先日pullリクエストがあったので、マージしてみました。

手順は、pullリクエストの画面に書いてあった(もう読めない)とおりにしました。

git checkout -b hidenorigoto-master master
git pull https://github.com/hidenorigoto/php-users.jp.git master
git checkout master
git merge hidenorigoto-master
git push origin master

pushするだけでPHP-users.jpのコンテンツが自動更新されるよう設定されているので、これで完了です。

もし「PHP-users.jpはもっとこうしたらいいのに!」というアイディアをお持ちの方がいらっしゃれば、ぜひ運営にご協力ください。pullリクエストでももちろん結構ですし、管理者である百式の田口さんにメールで名乗り出てもOKです(きっと)。

ちなみに

この日記に記事を書くのは久しぶりです。技術者以外の友人が多数でき、そちらに生活の重心が移ってしまいました。

技術者であることをやめたわけではないので、書くべきネタが見つかればまた書きます。なんかすみません。

第14回 xUnit Test Patterns 読書会に参加した

9月25日に開かれた「第14回 xUnit Test Patterns 読書会」に参加してきました。参加してよかったです。テストがうまく書けなくてモヤモヤしていたのがすっきりしました。

とくに印象に残った一言が、id:t-wada さんによる「インフラを疑わないというルールで書いてます」(Togetter参照)。「スタブのテストは書かないでいいのか」、「システム時刻が正しく取得できているかどうかをどうやって確認するのか」という(今にして思えばピントのずれた)僕の発言に対して、お答えいただいたものです。つまり僕は、テスト対象(SUT、system under test)だけでなく、依存コンポーネント(DOC、depended-on component)まで一緒くたにテストしたくなる病にかかっていたわけですね。

とまあ、読書会本来の主旨とは少しずれているのかもしれませんが、このようなモヤモヤを吹き飛ばしたいという狙いで初参加した僕にとっては、狙いどおりの成果が得られて助かりました。僕と同じような病気にかかっている方、この読書会はあと2回で終わる予定なので、ぜひ参加してみませんか?

xUnit Test Patterns: Refactoring Test Code (Addison-Wesley Signature Series (Fowler))

xUnit Test Patterns: Refactoring Test Code (Addison-Wesley Signature Series (Fowler))