Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

Phoenix.Channelで接続中のクライアント数を数える

御機嫌よう、ガラパゴスのおとめです。

今日は、PhoenixのChannel機能を触りつつ、いつもわたしたちを苦しめる「ある問題」に挑戦してみようと思います。

Phoenix.Channelのおさらい

さて、PhoenixのChannelについては既にいろいろな方が書かれていますが、簡単に言ってしまえば、WebSocketを使って複数のクライアントでリアルタイムに通信しましょう、ということになります。ちょっと簡易すぎるチャットなど実装してみましょう。公式サンプル通りのおさらいですのでサクッといきます。

まずはmix phoenix.newでプロジェクトを作成します。今回はchannel_sample_appというプロジェクトにしましたが、もちろんこの名前はお好みで。

$ mix phoenix.new channel_sample_app

デフォルトでweb/channels/user_socket.exが生成されますが、Channelはコメントアウトされていますので、コメントアウトを外します。

defmodule ChannelSampleApp.UserSocket do
  use Phoenix.Socket

  ## Channels
  channel "rooms:*", ChannelSampleApp.RoomChannel
  # ...

サーバ側の実装をしてしまいましょう。web/channels/user_socket.exのコメントを外してRoomChannelを使うようにしましたので、web/channels/room_channel.exに実装していきます。

defmodule ChannelSampleApp.RoomChannel do
  use Phoenix.Channel

  def join("room:chat", _message, socket) do
    {:ok, socket}
  end

  def handle_in("chat", %{"message" => message}, socket) do
    broadcast! socket, "chat", %{message: message}
    {:noreply, socket}
  end

  def handle_out("chat", payload, socket) do
    push socket, "chat", payload
    {:noreply, socket}
  end
end

デフォルトで生成されたweb/templates/page/index.html.eexを変更します。テキストボックスとメッセージを表示するブロックがあるだけの簡単なものです。

<div>
  <input class="form-channel" id="message" placeholder="なにしてはりますの?" type="text" />
</div>
<ul id="messages">
</ul>

web/channels/room_channel.exroom:chatというトピック名にしましたので、web/static/js/socket.jsをそれに合わせます。

// ...
let channel = socket.channel("room:chat", {})
channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

web/static/js/app.jsを実装します。テキストボックスでエンターキーが押されたら入力内容を送信して、受信したメッセージを追加していくだけの最低限な感じで。

//...
import socket from "./socket"
let channel = socket.channel("room:chat", {})
let message = document.getElementById("message")
let messages = document.getElementById("messages")
message.addEventListener("keypress", event => {
  if (event.keyCode === 13) {
    channel.push("chat", {message: message.value})
    message.value = ""
  }
})
channel.on("chat", payload => {
  let incoming = document.createElement("li");
  incoming.innerText = `[${Date()}] ${payload.message}`
  messages.appendChild(incoming)
})
channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })
export default socket

では起動してみます^1

$ mix phoenix.server

デフォルトでは4000番ポートで起動します。二つのブラウザからアクセスして、何か書いてみます。するとこのように別のブラウザにメッセージが表示されましたね?

f:id:glpgsinc:20160930192932p:plain

さて、これでごく簡単なチャットができました。実際にはユーザ管理ですとか、投稿内容の永続化ですとか、まだまだいろいろとやることはあるのですが、この記事ではそれらには触れず、別のことを見ていきます。

いま何人繋がっているの?

さて、プログラミングをされている皆様は、いつもいつもいつもいつも、「数を数える」という呪縛にとらわれているのではないかと思います。

その数、必要?

などと思うことも多々ありますが、今回紹介したようにチャット的な感じでChannelを使った場合、クライアント数のリアルタイム表示なんかは避けて通れないかもしれません^2。また数えるの……。

でも、接続数を数えるくらいならできそうな期待を胸に公式のドキュメントをざっと見ても、数を数える的なことは書いていないですね。

broadcastを追いかけてみる

ここに、Phoenixのソースコードがありんす。早速git cloneしてみましょう。

さて、先ほどのおさらいで、broadcast!/3をコールすると接続しているすべてのブラウザにメッセージが送られることがわかりました。ので、まずはここから見てみましょう。phoenix/lib/phoenix/channel.exに実装があります。

  def broadcast!(socket, event, message) do
    %{pubsub_server: pubsub_server, topic: topic} = assert_joined!(socket)
    Server.broadcast! pubsub_server, topic, event, message
  end

pubsub_serverをくっつけて、Server.broadcast/4をコールしていますが、pubsub_serverてなあに?

公式のドキュメントがリンク切れ^3なので、ソースコードをつつきまわしてみると、phoenix/lib/phoenix/endpoint.exに次のような魅力的な行が見つかります。

      @pubsub_server var!(config)[:pubsub][:name] ||
        (if var!(config)[:pubsub][:adapter] do
          raise ArgumentError, "an adapter was given to :pubsub but no :name was defined, " <>
                               "please pass the :name option accordingly"
        end)

おや設定に書いてある? それでは先ほど作ったプロジェクトのconfig.exsを見てみましょう。するとこのように書いてありますね(自動生成されたconfigです)。

  pubsub: [name: ChannelSampleApp.PubSub,
           adapter: Phoenix.PubSub.PG2]

pubsub_serverの正体がわかってきたところで、phoenix/lib/phoenix/channel/server.exを見てみましょう。するとこのように、PubSub.boradcast!/3をコールしていることがわかります。

  def broadcast!(pubsub_server, topic, event, payload)
      when is_binary(topic) and is_binary(event) and is_map(payload) do
    PubSub.broadcast! pubsub_server, topic, %Broadcast{
      topic: topic,
      event: event,
      payload: payload
    }
  end

Phoenixソースコードを見渡してもPubSubが見つかりませんか? その答えはmix.exsにあります。

  defp deps do
    [#...
     {:phoenix_pubsub, "~> 1.0"},
     #...]
  end

外部に切り出されていることがわかりました。こちらもgit cloneしてphoenix_pubsub/lib/phoenix/pubsub.exを追いかけていくと、ついにこのような関数に到達すると思います。

  def broadcast(server, topic, message) when is_atom(server) or is_tuple(server),
    do: call(server, :broadcast, [:none, topic, message])

これで、pubsub_serverbroadcastが目的地だとわかりました。先ほどpubsub_serverPhoenix.PubSub.PG2だということも突き止めましたので、おもむろにphoenix_pubsub/lib/phoenix/pubsub/pg2_server.exを見てみます。

  def broadcast(fastlane, server_name, pool_size, from_pid, topic, msg) do
    server_name
    |> get_members()
    |> do_broadcast(fastlane, server_name, pool_size, from_pid, topic, msg)
  end
  #...
  defp do_broadcast(pids, fastlane, server_name, pool_size, from_pid, topic, msg)
    when is_list(pids) do
    local_node = Phoenix.PubSub.node_name(server_name)

    Enum.each(pids, fn
      pid when is_pid(pid) and node(pid) == node() ->
        Local.broadcast(fastlane, server_name, pool_size, from_pid, topic, msg)
      {^server_name, node_name} when node_name == local_node ->
        Local.broadcast(fastlane, server_name, pool_size, from_pid, topic, msg)
      pid_or_tuple ->
        send(pid_or_tuple, {:forward_to_local, fastlane, from_pid, topic, msg})
    end)
    :ok
  end

ついに問題の核心部分に到達しつつあるように思えますね? ええと、なぜChannelのソースコードを追いかけていたのかというと、Channelに接続しているユーザの数を知りたいからでした。:pg2.get_members/1の結果をLocal.broadcast/6に渡しています。でも、ここに出てくるfastlaneとかserver_nameてなあに? どこから来るの?

ぐるりとUターンして出どころと正体を突き止めましょう。すると、先ほどあえて触れなかったcall(server, :broadcast, [:none, topic, message])に行き着きます。引数が幾つかなくなってそうな気がしますね? 実はこの引数の3番目のリストは、broadcast/6の4〜6番目の引数です。最初の三つはどこからくるのかと言いますと、phoenix_pubsub/lib/phoenix/pubsub/pg2.exにヒントがあります。見てみましょう。

    dispatch_rules = [{:broadcast, Phoenix.PubSub.PG2Server, [opts[:fastlane], server, pool_size]},
                      {:direct_broadcast, Phoenix.PubSub.PG2Server, [opts[:fastlane], server, pool_size]},
                      {:node_name, __MODULE__, [node_name]}]

:broadcastがきた時にどうするかが書いてありますね。そしてここにある三番目のリストがstart_link/2の最初の引数に当たります。じゃあstart_link/2はどこから来るのかというと、PubSubが起動した時になります。ちょっとphoenix_pubsub/lib/phoenix/pubsub/supervisor.exを見てみましょう。

  def start(_type, _args) do
    children = if pubsub = Application.get_env(:phoenix_pubsub, :pubsub) do
      [supervisor(Phoenix.PubSub.PG2, pubsub)]
    else
      []
    end
    opts = [strategy: :one_for_one]
    Supervisor.start_link(children, opts)
  end

出所がわかったところで、broadcast/6に戻ります。Local.broadcast/6を追いかけていくと……

  defp do_broadcast(fastlane, pubsub_server, shard, from, topic, msg) do
    pubsub_server
    |> subscribers_with_fastlanes(topic, shard)
    |> fastlane.fastlane(from, msg) # TODO: Test this contract
  end

  def subscribers_with_fastlanes(pubsub_server, topic, shard) when is_atom(pubsub_server) do
    try do
      shard
      |> local_for_shard(pubsub_server)
      |> :ets.lookup_element(topic, 2)
    catch
      :error, :badarg -> []
    end
  end

:ets.lookup_element/3が鍵を握っていそうです。このパラメタが何になるかというと、最初の引数はlocal_for_shared/2の戻りで、ここではpubsub_severをキーにしてローカルサーバーを取得しています。pubsub_serverは結局設定から読んでいることが先ほど分かりましたので、もう一度アプリケーションのconfig.exsを見てみましょう。

  pubsub: [name: ChannelSampleApp.PubSub,
           adapter: Phoenix.PubSub.PG2]

なあんだ、ていう感じですね。ChannelSampleApp.PubSubと書いてあります。二番目はトピック名ですので、web/channels/room_channel.exに書いたroom:chatになります。

確かめてみる

Elixirではモジュールでreqire IEx;してIEx.pryすることでデバッグコンソールに落とせますので、まずweb/channels/room_channel.exに書きます。

require IEx;
defmodule ChannelSampleApp.RoomChannel do
  # ...
  def handle_in("chat", %{"message" => message}, socket) do
    IEx.pry
    # ...

また、iexで起動する必要があります。

$ iex -S mix phoenix.server

handle_in/3にIExを仕込んだので、ブラウザから何か書いてみましょう。するとiexが起動します。:etsを触ってみましょう。

pry(1)> :ets.lookup_element(ChannelSampleApp.PubSub, 0, 2)
{ChannelSampleApp.PubSub.Local0, ChannelSampleApp.PubSub.GC0}

ローカルサーバはChannelSampleApp.PubSub.Local0だということが分かりました。これをもとにもう一度lookup_elementしてみると……

pry(2)> :ets.lookup_element(ChannelSampleApp.PubSub.Local0, "room:chat", 2)
[{#PID<0.360.0>, {#PID<0.357.0>, Phoenix.Transports.WebSocketSerializer, []}},
 {#PID<0.406.0>, {#PID<0.403.0>, Phoenix.Transports.WebSocketSerializer, []}}]

何やらリストが得られました。respawnしてデバッグコンソールから抜けて、ブラウザをもう一つ増やしてみると……

Interactive Elixir (1.3.3) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> :ets.lookup_element(ChannelSampleApp.PubSub.Local0, "room:chat", 2)
[{#PID<0.360.0>, {#PID<0.357.0>, Phoenix.Transports.WebSocketSerializer, []}},
 {#PID<0.406.0>, {#PID<0.403.0>, Phoenix.Transports.WebSocketSerializer, []}},
 {#PID<0.425.0>, {#PID<0.422.0>, Phoenix.Transports.WebSocketSerializer, []}}]

リストが増えましたね。では、この数を数えればいいのじゃないかしらん?

実装してみる

web/channels/room_channel.exに実装してみましょう。今回は簡単に、メッセージの末尾に数えた数を入れてみます。

  def handle_in("chat", %{"message" => message}, socket) do
    broadcast! socket, "chat", %{message: message <> "(#{length :ets.lookup_element(ChannelSampleApp.PubSub.Local0, "room:chat", 2)})"}
    {:noreply, socket}
  end

ブラウザからアクセスして何か書いてみましょう。

f:id:glpgsinc:20160930193228p:plain

見栄えは良くありませんが、無事に接続中クライアント数(らしきもの)が出てきました。

さいごに

今回は、Phoenix.Channelのソースコードを読んでみました。一緒にコードを読まれた皆様は、そのシンプルにすっきりしたコードに驚かれたことかと思います。

でもでも、今回の数を数える処理は、フレームワークの実装が変わるとアウトですね。また、PubSubアダプタにPG2以外を指定した時も、もちろん動きません。実際にはRedisか何かにsocket.channel_pidか何かを入れて数を数える方が良いでしょう。

さて、ガラパゴスでは、一緒にいろいろなコードを読む仲間を大絶賛超募集しています。皆様の応募をお待ちしています。

RECRUIT | 株式会社ガラパゴス iPhone/iPad/Androidのスマートフォンアプリ開発

では、御機嫌よう。

RubyKaigi2016のまとめ

はじめに

こんにちは、@vanhuyzです。 ガラパゴスのサーバーサイドではRubyがメインですから、是非一度もRuby会議参加したいと思い、今年2016年度のRuby会議@京都に参りました。 初参加で緊張ではありますが、楽しかったです。

会場の様子 f:id:glpgsinc:20160908095943j:plain

会場の外見 f:id:glpgsinc:20160908124634j:plain

せっかく参加したので、聴いた内容をまとめていきたいと思います。

Rubyの未来について

基調講演 Ruby3の型について “Ruby3 Typing” by @Matz

Ruby3の目標は:Soft Typing、 Ruby3x3(3倍速く)とConcurrencyです。Soft Typingというのは現在RubyDuck Typingを継承し、型チェックを加えるというアイデアです。要はプログラミングを実行する前に、型推論して型チェックする仕組みです。また、型情報をデータベース化するという考えもありました。これで、動的型の柔軟性を保つことができ、静的型のようにコード自体がドキュメンテーションになるということです。まだふわふわではありますが、Ruby3楽しみですね!ちなみに、Ruby3いつ来るかというとMatz氏自身もまだわからなく、Tokyo Olympicsまで目標したいらしいです。

f:id:glpgsinc:20160912214447p:plain

Ruby2.4の変更点: Unifying Fixnum and Bignum into Integer by @tanaka_akr

Ruby 2.4からFixnumとBignumがIntegerに統一することになるという話です。現状整数のクラスは2つ存在し、Fixnumは通常の整数で、BignumはFixnumの範囲外の整数を扱うクラスです。Fixnumの範囲はRubyの実装(CRuby、JRubyなど)により異なるので、学習コスト・ドキュメンテーションコストが高かったです。これから、全部Integerに統一することで、学習が簡単になるでしょう。唯一の問題はincompatibility(不適合性)です。 例えば、object.is_a?(Fixnum) のようなコードは意図通り動かないかもしれません。

発表スライド:Unifying Fixnum and Bignum into Integer

Concurrency(並列処理)

f:id:glpgsinc:20160912171635p:plain

ConcurrencyはRubyの弱点(?)の一つなので、今年はconcurrencyについてたくさんのセッションがありました。

始めに、@ko1氏はguildモデルを提案しました。主にmutableなオブジェクトを管理する仕組みです。もちろんimmutableなオブジェクトはguildの間にシェアできます。Guild間の伝達(コピー・移動)はチャネル上に行います。Guildの詳細は公開スライドA proposal of new concurrency model for Ruby 3をご覧ください。

次に、@anildigital氏によりいろんな言語のconcurrencyモデルを比較しました。JavaClojure、 Node.js、Python、Elixir、Goなどです。Rubyにはconcurrent-rubyというgemがあります。発表はちょっと長くなりましたが、concurrencyについてよくまとめました。本当に貴重な資料なので一度目を通すと良いです。

最後に、言語のconcurrencyの話ではなく、@wyhaines氏によりWebサーバーのconcurrencyアーキテクチャにていて発表されました。現在、concurrencyモデルは4つあります: Blocking Single Threaded、 Event Driven、Multiprocess、 Multithreaded です。どれを採用すべきか用途によりますが、一般的にMultithreadedが一番良さそうだと思います。@wyhaines氏は今ScrawlsというシンブルなRuby Web Serverを開発しています。これを使うと、どのconcurrencyモデルも指定でします。 さらに、現在のRubyのWebサーバーをリストアップされました。WEBrick、Puma、Passenger、Unicorn以外にもたくさんのWebサーバーが存在していますね。

スライドはWeb Server Concurrencyに公開されました。

他のRuby言語関連の話

基調講演 Rubyリファクタリング “Fearlessly Refactoring Legacy Ruby” by @searls

Day2の基調講演で@searls氏がRubyリファクタリングについて発表しました。現代、レガシーコードは避けられないことが多いでしょう。リファクタリングはいつも難しい+大変な仕事です。リファクタリングがズムーズに行うために、@searls氏がsutureというgemを作りました。このgemで9ステップでリファクタするという話です。詳細は以下のスライドです。

感想ですが、テストの無いコードをリファクタするときsutureはとても良いと思います。sutureの良い点は短いコードでカバレッジ100%にできますが、リファクタ後全部の確認コードを捨ててしまうので再度使えないようです。もしいつかリファクタしたいならもう一度suture書かないといけないということになります。複数回sutureコード書くならテストコードを書いた方が長期的に良いと思います。sutureコードからテストコードに自動生成機能があれば超うれしいです。

RubyインタプリタErlangで作った話 “ErRuby - Ruby on Erlang” by @johnlinvc

目的はRubyからErlangに訳します。Rubyはかなり複雑な言語ですからパーサーは超難しいそうです。現在、ロカール/インスタンス変数、メソッド定義・呼び出し、クラスと継承、ブロックとyieldが実装完了です。Boolean、Integer、String、Arrayの一部のメソッドもできました。さらに、Futureオブジェクトという機能を実験中です。

ソースコードはこちらです https://github.com/johnlinvc/erruby

String表現の話 “A Tale of Two String Representations” by @nirvdrum

2つのstring表現があります: RStringとRopeです。RStringは多分普通のstringで、mutableで、フラットな表現のに対し、Ropeはimmutableで、木構造で表現します。Ropeの表現のメリットは使用メモリが減少し、メタプログラミングができ、thread-safeです。現状はJRuby+Truffle & Graalで問題なく機能しています。

Rubyで作ったものの紹介

Rubyでコンテナを作る “Welcome to haconiwa - the (m)Ruby on Container” by @udzura

発表者はコンテナ技術を深く理解するために、独自コンテナをつくりました。haconiwaと呼ばれています。haconiwaの最初はCRubyで実装しましたが、syscallsの限界がありまして、結局全部mRubyで書き直しました。mRubyはシステムプログラミングに最適だそうです。

haconiwaのソースコード https://github.com/haconiwa/haconiwa

Docker上のキュー管理システム “Scalable job queue system built with Docker” by @k0kubun

Dockerを導入して、Docker用job queue systemがほしいというきっかけでbarbequeを作りました。barbequeはAmazon SQSと連携し、auto scalingも対応されるという素晴らしいツールです。

Rubyゲームボーイエミュレータを作る話 “Writing A Gameboy Emulator in Ruby” by @0xColby

ゲームボーイはCPU、メモリ、Picture Processing Unit、スクリーン、カートリッジというコンポネントからなり、各コンポネンの実装を示しました。最終的に全部結合してloopの中で回す感じです。Rubyはこういう用途ができるとは思わなかったです。

ソースコードはこちらです https://github.com/colby-swandale/waterfoul

“Game Development + Ruby = Happiness” by @amirrajan

発表者はA Dark Roomというゲーム、#1 AppStoreになったことあるゲームの開発者です。このゲームはiOSですが全てRubyで開発したそうです。RubyMotionを使ったそうです。発表者によりC#JavaScriptObjective-Cなどでゲーム開発するのははすごく大変でした。Rubyは簡潔でとても良いです。

スライドはこちらです slides.com

Big Data + 機械学習

BigQueryの紹介 “Exploring Big Data with rubygems.org Download Data” by @thagomizer

何のgemがダウンロード数が一番高いやMinitestまたはRspecどちらの方人気が高いなどという質問でrubygems.orgのデータを使って、BigQuery上で解析するという話です。BigQueryはSQLで、速い、スケール可能、十分な機能を持つという特徴があります。RubyでBigQueryを使うにはgoogle-cloud gemを使えばよいです。データ解析結果より、rspecよりminitestの方が人気だそうです。

スライドはこちらになります http://www.thagomizer.com/files/ruby_kaigi_2016.pdf

Data Analysis in Ruby with daru

Rubyでデータ解析するため、daruというツールを作りました。daruは1Dベクトル・2Dベクトルのデータ構造を持っていて、データの基本的な計算(平均など)とグラフ化ができます。さらに、daruはiruby notebook(ipython/jupyterのようなインタラクティブシェル)と他のRuby機械学習ライブラリを連携できます。デモとしてiruby notebook上にロジスティック回帰というアルゴリズムを実行されました。

発表者はこのツールを作ったとき、RubyでCバインディングがかなり面倒だとわかっていて、現在rubexを開発しています。rubexはPythonのCythonのような位置付けでしょう。

スライドはこちらです。

Rubyでの機械学習 “SciRuby Machine Learning Current Status and Future” by @mrkn

現在の機械学習でよく使われる言語といえばPythonでしょう。Pythonのscikit-learnというライブラリは機械学習の基盤となり、とても使いやすく他のライブラリと互換性があるということです。 Pythonで実際の機械学習ワークフロー全部できる状態になっています。それに対してはRubyはまだできない状況です。これから、SciRubyの発展はどうすべきか2つの方針を述べました。

  • Rubyでscikit-learnそのものを使えるようにする
    • JuliaからPythonのものを使える事例があるから
  • ゼロからscikit-learnのようなものを作る
    • これは辛い仕事
    • Cython-like systemが必要
      • 上の発表のrubexを完成する
    • 数値配列計算ライブラリが必要
      • 現在いくつ存在します(NMatrix、Numo::NArray、NumBuffer)が、使うものにはならない

感想:私はWebアプリケーションを作るとき、Rubyがメインですが、最近機械学習・深層学習の実装が増えてきて、やはりPythonはベストチョイスです。ライブラリの充実さは半端ないです。ベクトル・配列計算はNumPyがとても便利です。さらに、TensorFlowという深層学習フレームワークAPIPythonです。PythonよりRubyの方が好きですが、やはりRubyの現状は難しいですね。SciRuby、頑張ってください!応援します!

さいごに

ガラパゴスでは、Rubyistはもちろん、他の言語も挑戦したい方を絶賛募集しています。皆様の応募をお待ちしています。

RECRUIT | 株式会社ガラパゴス iPhone/iPad/Androidのスマートフォンアプリ開発

セキュリティの大脅威と予防方法 (Ruby On Rails向け)

はじめに

こんにちは、ガラパゴスのジョンです。 Webアプリケーションを開発するときに、使いやすさや速さや可用性などをよく考慮されますが、今回Webアプリケーションに関わるセキュリティの脅威とその予防方法について考慮しましょう。

特にIoTの導入と共に、キュリティの脅威の範囲がPCとスマホに限らないで、我々の生活の周りのものまで障害を与える恐れがあります。

Webアプリケーションに関わるメインな3つの脅威をそれぞれの特徴とRuby On Rails環境での予防方法をこれから具体的に説明します。しかし、他の環境でも予防方法の基本は同じです。

SQLインジェクション

DBを使うWebアプリがSQLクエリでDBのデータ処理をします。 SQLクエリを生成するにはユーザーからの入力が必要な場合が多いです。 例えば、ログイン画面でE-mailとパスワードを入力し、E-mailで検索するSQL文を生成し、検索結果のパスワードの認証が行うユースケースです。

Ruby On Railsではユーザー検索するためのコードは次のようです。

user = User.where("email = '#{params[email]}'")

ユーザーが特別文字を入力しない場合に次のようなクエリが実行されて、認証操作が正常にします。

SELECT * FROM users WHERE (email = 'hoge@example.com')

しかし、攻撃者がいて、メールアドレスに次のような入力をする場合、

hoge@example.com' OR 1 = 1) LIMIT 1 OFFSET ('0
hoge@example.com' OR 1 = 1) LIMIT 1 OFFSET ('1
               ...

と実行されるクエリは次のようになります。

SELECT * FROM users WHERE ('hoge@example.com' OR 1 = 1) LIMIT 1 OFFSET ('0')
SELECT * FROM users WHERE ('hoge@example.com' OR 1 = 1) LIMIT 1 OFFSET ('1')
               ...

そのクエリの実行結果がUsersテーブルの一番のリコードを取得し、次2番のリコードなどです。その方法で攻撃者が簡単に推測できるパスワードのユーザーアカウントにログインできるまで、繰り返すことができます。

データの盗難の攻撃に留まらず、データの破壊攻撃も次のような可能です。

例えば、ユーザーが自分の作成した記事を削除ボタンを押す時に次のコードが実行されます。

Article.destroy_all("user_id = '#{current_user.id}' AND id = '#{params[:id]}'")

idパラメータに有効な記事IDがある場合に、そのコードが現在ユーザー「current_user」の記事であれば、指定された記事を削除します。

しかし、攻撃者がidに次のデータに挿入する場合、

22' OR 1=1) --

と実行されるクエリは次のようになります。

SELECT "articles".* FROM "articles" WHERE (user_id='2' id = '22' OR 1=1) --')

[OR 1=1] の部分がWHERE文の条件をいつもTRUEにします。また、--部分の後はコメント文になります。 つまり、攻撃者の入力された文で実行されるクエリが他のユーザーの記事を含めて、全ての記事をマッチするので、全てが削除されてしまいます。

それ以外のSQLインジェクション攻撃の方法が様々があります。詳しくは こちらです。

対策方法

基本ルールはユーザーの入力を信じてはいけません。すなわち、入力されたデータをデータとして扱われることの確認が必要です。Ruby On Railsではそうできるために、いろいろな方法が提供されています。次にそれぞれの方法を説明します。

ハッシュの使用

上記の例ではSQL直接を書きましたがそうではなく、パラメッタをハッシュ構造でwhereとdestroy_allなどの関数に渡します。

user = User.where(email: params[email]})

Placeholderの使用

SQL文が複雑な時にハッシュ化が不可になる場合があります。その場合に?記号とパラメッターを使えばSQLインジェクションを防ぐことができます。

Article.destroy_all(["user_id = '?' AND id = '?'", current_user.id, params[:id]])

SantizeまたはQuoteの使用

user = User.where("email = #{User.sanitize(params[email])}")
user = User.where("email = #{User.connection.quote(params[email])}")

CSRF攻撃(クロスサイトリクエストフォージェリ)

"Exploit the trust a web server has in browser"

CSRFはWebアプリケーションに存在する脆弱性の一つです。被害者が攻撃用のWebサイトを閲覧することにより、被害者の意図ではない操作が脆弱性のあるWebサイトが受け付けてしまいます。 通常に攻撃Webサイトで送信される要求により状態の変化が行います。例えば、ユーザーの意図せずに商品を注文するまたは、ユーザーの銀行口座から振込するなどの操作です。

現在のブラウザーのセクリティー制約上で、データを取得する要求「GET操作」するだけで、攻撃者がそのデータが見ることができないので、攻撃者にとって意味がありません。 これで、POST操作がCSRF攻撃対象になることはほとんどです。

次に説明するXSS攻撃とCSRF攻撃を一緒に使われたら、攻撃者がGET操作でデータを取得できるので、CSRFXSS攻撃が両方を同時に使われることはよくあります。

f:id:glpgsinc:20160831150030p:plain

次の例では「onlinebank.example.com」がCSRF脆弱性のあるオンライン銀行の例です。攻撃者が次のコードを攻撃用のWebサイトに用意し、メールなどで被害者に開くようにリンクを送信します。 被害者がそのページを開くだけで、http://onlinebank.example.com/transferへ振込要求が送信されて、銀行のWebサイトがCSRF脆弱性あるため、ユーザーの意図ではない振込を受け付けてしまいます。

<html><body>

  <form name="csrf_attack_form" action="http://onlinebank.example.com/transfer" method="POST">
    <input type="hidden" name="amount" value="100000">
    <input type="hidden" name="transferee_id" value="[attacker_bank_id]">
  </form>

  <script type="text/javascript">document.csrf_attack_form.submit();</script>
</body></html>

次の予防方法がCSRF攻撃を起こりにくくしますが、完全に予防しません。

1- Same Origin Policy「同一生成元ポリシー」 Same Origin PolicyとはAJAX要求しているページのロード元とAJAX要求の先が同じスキーム + ドメーンでいなければならないという意味です。 例えば、

    http://example.com/dynamic_page

をロードすることにより、AJAX要求が送信される場合に

    http://example.com/ajax

 に許可されますが、

    http://example2.com/ajax

が違うドメインなので、ブラウザのセキュリティ上で許可されません。その方針のおかげでCSRF用のページが攻撃対象のWebアプリケーションにAJAX要求が送れません。

2- X-Frame-Options: SAMEORIGIN AJAX要求と同じように攻撃者のWebサイトより、銀行などの攻撃対象のWebサイトをIFRAMEに表示すること予防するためにHTTPヘッダーに「X-Frame-Options: SAMEORIGIN」送るわけです。

X-Frame-Options: SAMEORIGIN

上記の対策でAJAX、IFRAMEでCSRF攻撃をできなくなりましたが、フォームの送信で攻撃は可能なので次のように対策方法を説明します。

対策方法

POST、PUT、PATCH要求を受け付ける時に攻撃者が推測できないトークンを用います。そのトークンを申請フォームなどのフィールドとして、追加します。要求を受け付ける時にトークンの認証が行って正しくなければ、要求を拒否します。Same Origin Policyのおかげで、攻撃者が攻撃対象のWebサイトにより、ユーザー側に保存されたクッキーを見れませんので、CSRFトークン認証することでその攻撃を予防できます。

Ruby On RailsではCSRF攻撃の対策がコア機能に組み込まれているので、各コントローラーに次の行を追加するだけでCSRFの攻撃予防ができます。

しかし、GET要求がCSRF攻撃に狙われる価値が普段にないので、CSRF攻撃予防の対象になっていません。つまり、Webアプリケーションの設計ルールとして、GET要求で状態変更する機能を作ることは避けるべきです。 また、XSS脆弱性があれば、CSRF攻撃予防を回避することは可能になってしまう場合があります。

class ApplicationController < ActionController::Base
  protect_from_forgery
end

XSS攻撃(クロスサイトスクリプティング

"Exploit the trust a browser has in a legitimate website"

XSSとは信頼性のあるWebサイトに悪意のあるコードを埋め込む攻撃方法です。 XSS攻撃の書類が3つあります。

タイプ1:Reflected XSS攻撃

埋め込まれるコードが直接URLのパラメータとして提供される場合にReflected XSS「反映的なXSS」と言います。

f:id:glpgsinc:20160831150053p:plain

次のデモ用のURLが検索のWebアプレケーションです。queryのパラメータに検索キーワードを入力すれば検索してくれて結果がない場合に、"No result found for [入力したケーワード]"が表示されます。

Hello Worldを検索する

ページの内容は次のようになります。

<div>
Sorry, no results were found for <b>Hello World</b>.
...
</div>

f:id:glpgsinc:20160831150139p:plain

しかし、攻撃者が悪性のコードを用意し、queryパラメータに埋め込んで、作成したURLを被害者をクリックさせる場合、

Reflected XSSを試す

queryパラメータに適切な検証せずにそのまま、ページに埋め込んでしまいした。その結果、攻撃者が用意したJAVASCRIPTコードが被害者のブラウザーに送信されて、実行されてしまいます。

被害者のブライザーに送信されるページの中身は次のようです。

<div>
Sorry, no results were found for <b><script>alert('Reflected XSS')</script></b>.
...
</div>

f:id:glpgsinc:20160831150201p:plain

タイプ2:Persistent / Stored XSS攻撃

悪性のあるコードをコメントなどで入力し、データベースに保存されます。これで、被害者が信頼しているページを見るときに攻撃者の入力が表そのまま、ブラウザーに送信されることで悪性のあるコードが実行されてしまいます。

f:id:glpgsinc:20160831150214p:plain

次の例では「Persistent / Stored XSS攻撃」を試すことができます。"Google Chromeのみに動きます"

下記のURLではコメントを書く簡単なWebアプリケーションです。

https://xss-doc.appspot.com/demo/1

例えば、

Hello Guys

を書くとコメントの一覧に表示されます。

f:id:glpgsinc:20160831150244p:plain

しかし、ユーザー入力をヴァリエーションせずにコメントをそのまま表示しているため、XSS攻撃の脆弱性があります。

Javascriptのコードをコメントに入力すると、今度そのページを見る人のブラウザーで、入力したコードが実行されてしまいます。

f:id:glpgsinc:20160831150309p:plain

コードのバリエーションがいっぱいあるため、すべてを予防することは簡単ではありません。次はコードのバリエーションの一部です。

<iframe src="data:text/html;base64,PHNjcmlwdD4NCmFsZXJ0KCJYU1MiKQ0KPC9zY3JpcHQ+" />
<img src="http://example.com/nonexistent.png" onerror="j&#X41vascript:alert('Persistent XSS');" />
<META HTTP-EQUIV="refresh"
CONTENT="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg">

Samy Worm

Myspaceが特定なHTMLタグの入力を自分のプロフィールに追加することを許可しましたが、SCRIPTタグ、EVENT属性「onclick」、「onload」、「onなんでも」などを積極的にフィルターしました。 しかし、「Samy Kamkar」さんがJAVASCRIPTコードをSTYLEタグに埋め込んで、自己複製のWormを工夫しました。そのWormを自分のプロフィールに埋め込んだら、20時間以内に1,000,000人がそのWormを実行してしまいました。 技術的な明細情報はこちらです。

「Persistent / Stored XSS攻撃」と「Reflected XSS攻撃」が悪性のあるコードがサーバー型で処理されて、ユーザーのブラウザーに漏れるため、サーバー側の脆弱性と見なしますが、クライント側でもXSS攻撃があります。

タイプ0:DOM Based XSS攻撃

その一方、サーバー側で脆弱性ではなく、クライント側で実行されるコードの脆弱性でパラメッターなどの処理によって、DOMに悪性のあるコードが埋め込まれて、実行されてしまうことはDOM Based XSS攻撃だと言います。

例えば、次の例が3つの画像の一つを表示しています。#記号の後の番号で、現在表示している画像を決まります。その操作はJAVASCRIPTコードで実装されています。

画像1表示するには https://xss-doc.appspot.com/demo/3#1

f:id:glpgsinc:20160831150331p:plain

画像2表示するには https://xss-doc.appspot.com/demo/3#2

f:id:glpgsinc:20160831150343p:plain

などです。

しかし、次のURLを開くと、URLに埋め込まれたJavascriptコードが実行されてしまいます。

f:id:glpgsinc:20160831150356p:plain

DOM Based RSS攻撃のデモがこちらです。

#記号の後の部分はサーバーに送られないため、サーバーが悪性あるコードを検出することはできません。

XSS攻撃の予防方法

XSSのタイプによって、対応方法が異なりますが、こちらは一般なガイドラインです。

"<script>alert('XSS')</script>".html_safe
raw "<script>alert('XSS')</script>"

特定なタグを許可し、それ以外のタグを削除したければ、sanitizeメソッドを使えばいいです。

sanitize("<p>safe code</p><script>alert('unsafe code')")</script>

結果は

<p>safe code</p>

ブラウザーに送信されます。

Ruby On Railsでコードのフィルタリングのメソッドがいろいろあります。詳しくはこちらです。

また、文字列をJAVASCRIPTに埋め込む際に「escape_javascript」メソッドを使用すべきです。

<script>
var x = "<%= escape_javascript(unsafe_data) %>";
...
</script>
  • ユーザーからのパラメータをバリデーションします。
  • HTMLコードをユーザーから拒否し、マークダウンなどのセーフな記法を使用します。
  • Ruby On Railsでは悪性のあるコードがDBに存在するかどうかをスキャンしてくれるGemはこちらです。
  • X-XSS-Protectionヘッダーの追加: タイプ1(Reflected XSS攻撃)を予防するために「XSS Auditor」という機能が現在のブラウザに埋め込まれています。サーバーからのHTTPレスポンスにX-XSS-Protectionヘッダを指定することで、その機能を有効にできます。
X-XSS-Protection: 1; mode=block

参考文献

https://www.coursera.org/learn/software-security

http://chris.vandenberghe.org/publications/csse_raid2005.pdf

Ruby on Rails Security Guide — Ruby on Rails Guides

http://rails-sqli.org/

Cross-Site Request Forgery (CSRF) - OWASP

Types of Cross-Site Scripting - OWASP

https://www.owasp.org/index.php/Ruby_on_Rails_Cheatsheet

https://www.ipa.go.jp/files/000024729.pdf

http://kaworu.jpn.org/security/X-XSS-Protection

https://www.google.com/about/appsecurity/learning/xss/