株式会社クリアコード > ククログ

ククログ


Firefoxのアップデートのしくみについて

はじめに

Windows版のFirefoxでは、メニューから「Firefoxについて」を選択すると、最新のバージョンのチェックを行えます。 この際、更新があればアップデートを適用できるようになっています。*1

今回は、Firefoxのアップデートのしくみがどのようになっているのかを以下の技術資料を参考に紹介します。

Firefoxアップデートの流れ

Firefoxの更新がある場合、以下の流れで適用されます。(手動で明示的に更新する場合)

  • 「Firefoxについて」をメニューからクリック
  • Firefoxが更新サーバにバージョンチェックのリクエストを出す
  • 更新サーバがバージョンチェック結果をレスポンスとして返す
  • Firefoxはレスポンスを確認して、アップデートがあればアップデートファイルを更新サーバにリクエストする(なければなにもしない)
  • 更新サーバがアップデートファイルを返す
  • Firefoxはアップデートファイルをダウンロードし、ダイアログに「Firefoxを再起動して更新」ボタンを表示する
  • ユーザーがボタンをクリックするとFirefoxを再起動、更新を適用する

ここでポイントとなるのがバージョンチェックとアップデートファイルのダウンロードの部分です。 それぞれもう少し詳しく説明します。

バージョンチェックのリクエスト

バージョンチェックでは、実際にどのようなリクエストを更新サーバに出しているのでしょうか。実際に確認してみましょう。 これはURL入力欄に about:config とタイプして設定画面を表示させ、 app.update.url を検索してみるとわかります。 app.update.url はバージョンチェックするためにアクセスするURLです。

45.6.0ESRの場合は以下のとおりです。*2

https://aus5.mozilla.org/update/6/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%(nowebsense)/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml

いくつか %XXX% というパラメータがあります。これはリクエストするときに実際の値に置換されます。それぞれの意味は次の通りです。

  • %PRODUCT% プロダクト名です。「Firefox」となります。
  • %VERSION% 45.6.0ESRの場合「45.6.0」です。
  • %BUILD_ID% 45.6.0ESRの場合は「20161209150850」です。
  • %BUILD_TARGET% 32bit版のFirefoxでは「WINNT_x86-msvc-x64」です。64bit版だと「WINNT_x86_64-msvc-x64」です。
  • %LOCALE% 日本語版なので「ja」です。
  • %CHANNEL% どのエディションなのかを示す値です。ESR版なので「esr」です。
  • %OS_VERSION% 使用しているOSを示す値です。Windows 7 64bit版だと「Windows_NT%206.1.1.0%20(x64)」です。
  • %SYSTEM_CAPABILITIES% 環境によっておそらく異なる値となります。手元の環境では「SSE3」でした。
  • %DISTRIBUTION% Firefox公式なので「default」です。
  • %DISTRIBUTION_VERSION% Firefox公式なので「default」です。

したがって、実際のリクエストは以下のようになります。

https://aus5.mozilla.org/update/6/Firefox/45.6.0/20161209150850/WINNT_x86-msvc-x64/ja/esr/Windows_NT%206.1.1.0%20(x64)(nowebsense)/SSE3/default/default/update.xml

このとき実際に更新サーバから返ってくるレスポンスは以下のとおりです。

<?xml version="1.0"?>
<updates>
    <update type="minor" displayVersion="45.7.0esr" appVersion="45.7.0" platformVersion="45.7.0" buildID="20170118123525" detailsURL="https://www.mozilla.org/ja/firefox/45.7.0/releasenotes/">
        <patch type="complete" URL="http://download.mozilla.org/?product=firefox-45.7.0esr-complete&amp;os=win&amp;lang=ja" hashFunction="sha512" hashValue="6168bcaa9424fe1d789c80cf54fe55c8b4e70f2d5468ebbeaef1f2776a3a840806b1ac270306852684a45ff9ad050de3f46a149dc2747d4a6b9440e0c27bf8a5" size="52388819"/>
        <patch type="partial" URL="http://download.mozilla.org/?product=firefox-45.7.0esr-partial-45.6.0esr&amp;os=win&amp;lang=ja" hashFunction="sha512" hashValue="885218494c4b2b8ecd9c1c696b84f5850e78898cd289e28aa6cd8b6f727fa4dbd363de3edb6da861456ab01bb2c38639fbdc93f01746e89e7de4c3d836fceab5" size="6470506"/>
    </update>
</updates>

更新対象として、45.7.0ESRが存在していることがわかります。*3

次のようにコマンドラインからパラメータを明示してバージョンチェックのリクエストを投げることもできます。*4

curl "https://aus5.mozilla.org/update/6/Firefox/45.6.0/20161209150850/WINNT_x86-msvc-x64/ja/esr/Windows_NT%206.1.1.0%20(x64)(nowebsense)/SSE3/default/default/update.xml"
アップデートファイルのダウンロード

レスポンスの例で示したように、更新サーバはアップデートファイルへのリンクを返します。アップデートがなければ以下のように空のタグを返します。

<?xml version="1.0"?>
<updates>
</updates>

アップデートがあるときには差分版もしくは完全版がダウンロードされますが、これらはどこから入手したらよいのでしょうか。

これには、CDN経由でダウンロードするやりかたがあります。以下のような所定のディレクトリ階層でアップデートファイルが提供されています。

http://download.cdn.mozilla.net/pub/firefox/releases/(ESRのバージョン)/update/(32bit or 64bit)/(言語)/

したがって45.7.0ESRの場合、45.6.0ESRから45.7.0ESRへの差分版と45.7.0ESRへの完全版のアップデートファイルが以下から入手できます。

http://download.cdn.mozilla.net/pub/firefox/releases/45.7.0esr/update/win32/ja/

このディレクトリには以下の3つのファイルがあります。

  • firefox-45.6.0esr-45.7.0esr.partial.mar
  • firefox-45.6.0esr-45.7.0esr.partial.mar.asc
  • firefox-45.7.0esr.complete.mar

このうち、アップデートファイルは拡張子が.marのものです。marとは Mozilla ARchive に由来しています。 marファイルのフォーマットに興味がある方は、Software_Update:MARを参照するとよいでしょう。

firefox-45.6.0esr-45.7.0esr.partial.mar は45.6.0ESRから45.7.0ESRへ差分更新する際に適用されるファイルで、 firefox-45.7.0esr.complete.mar は45.7.0ESRへ完全版でアップデートするときに適用されるファイルです。

まとめ

今回は、Firefoxのアップデートのしくみについて紹介しました。 このしくみを利用すれば、企業内でFirefoxの更新サーバを独自に立てることも可能です。 そのあたりの話は別の記事にて紹介したいと考えています。

*1 Linuxディストリビューションの場合など、アプリケーションの管理ポリシーによっては無効になっていることがあります。

*2 2017年3月時点では45.7.0ESRが最新ですが、バージョンチェックの確認のため一つ前のバージョンを例に出しています。

*3 partialというのは差分版、completeは完全版のことです。例えば45.6.0ESRから45.7.0ESRは1世代前なので差分更新できますが、45.5.0ESRからは2世代前なので完全版でアップデートします。

*4 45.3.0未満だといったん45.3まで上げてから最新版へとアップデートされることに気がつくことでしょう。

2017-04-04

2017年4月12日頃のApache Arrow

2017年4月12日頃のApache Arrowの様子を紹介します。

arrow::Tensorの追加

0.2.0頃のApache Arrowはarrow::Arrayで1次元のデータ(配列)、arrow::Tableで2次元のデータ(表)を表現していました。最近のApache Arrowはこれらに加えてarrow::Tensorを追加しました。これはN次元のデータを表現します。

arrow::Tensorは以下と同じようなデータを表現します。

Apache Arrowはシステム間でのデータ交換のコストを下げることを重視しています。つまり、最近のApache Arrowはarrow::Tensorで表現するようなデータのデータ交換コストも下げる取り組みを始めた、ということです。

現時点のarrow::Tensorはゼロコピーでのデシリアライズに対応しています。Rayという分散タスク実行エンジンはNumPyのデータをシリアライズするためにApache Arrowを使うようにしました

今後はarrow::Tensorのデータに対して数学関数を使えるようにする予定です。要素毎(element-wise)の演算だけでなく行列演算もサポートするかどうかはまだわかりません。

サブライブラリーの統合

0.2.0までのApache Arrowはlibarrowとlibarrow_io(入出力用)とlibarrow_ipc(シリアライズ・デシリアライズ用)というライブラリーに分かれていましたが、libarrowに統合されました。Apache Arrowを使う場合は全部使うことが多いので、これでシンプルに使えるようになりました。

まとめ

2017年4月12日頃のApache Arrowの様子を紹介しました。そろそろ0.3.0がでそうなのですが、arrow::Tensorは0.3.0の目玉になりそうです。

2017-04-12

fluent-plugin-geoip の geoip2 対応した話

fluent-plugin-geoipというIPアドレスから国や州・県などの情報を取得してレコードを加工するFluentdのプラグインがあります。

以前はGeoIP Legacyにあるデータベースを使ってIPアドレスから情報を取得していましたが、GeoIP2がリリースされてしばらく経過したのでGeoIP2に対応した話を書きます。

geoip2_cの開発

経緯はSupport GeoLite2 format · Issue #39 · y-ken/fluent-plugin-geoipに書いてありますが、少し抜粋します。

GeoIPについて調べているとGeoIP2を見つけ、さらにGeoIP2に対応したfluent-plugin-filter-geoipを見つけました。 そこで、fluent-plugin-filter-geoipの内部を調べ、どのようにしてGeoIP2に対応させているかを確認したところmaxminddbというピュアRuby実装のライブラリを使っていました。 他にGeoIP2を利用できるライブラリがあるかどうかを調査したところいくつか既存の実装がありました。

新たにfluent-plugin-geoipにGeoIP2対応を追加するにあたって、GeoIPを使用していたときと同等の性能を維持できるかどうかを確認するためにベンチマークをとりました。

ベンチマークによると、geoip2_compatであれば性能に問題はなさそうなことがわかりましたが、geoip2_compatだとGeoIP Legacyと同等のデータしか取得することができません。GeoIP2にはそれ以外のデータも多数追加されているので、できれば全ての機能を使えるようにしたいと考えていました。maxminddbとhive_geoip2はGeoIPよりも遅くなってしまうので使えません。maxmind_geoip2は速度的には問題なさそうでしたがAPIが独特で使い辛い感じでした。

それぞれの拡張ライブラリのコードを読んでみたところ、遅くなっていた原因は全ての属性値を取得していたことでした。速くするためには、必要な属性値のみ取得するようにすればよいはずです。 この仮説を検証するためにgeoip2_cを開発しました。

先程のベンチマークにgeoip2_cを追加したベンチマークによるとgeoip2_cが最速です。

Rehearsal ---------------------------------------------------------
geoip                   0.140000   0.000000   0.140000 (  0.147379)
geoip2_compat           0.110000   0.010000   0.120000 (  0.108135)
maxminddb (pure ruby)   4.310000   0.000000   4.310000 (  4.320897)
hive                    0.320000   0.000000   0.320000 (  0.321934)
maxmind_geoip2          1.240000   0.320000   1.560000 (  1.561630)
geoip2_c                0.070000   0.000000   0.070000 (  0.067715)
------------------------------------------------ total: 6.520000sec

                            user     system      total        real
geoip                   0.140000   0.000000   0.140000 (  0.142973)
geoip2_compat           0.160000   0.000000   0.160000 (  0.162996)
maxminddb (pure ruby)   4.650000   0.000000   4.650000 (  4.654088)
hive                    0.310000   0.000000   0.310000 (  0.308363)
maxmind_geoip2          1.350000   0.430000   1.780000 (  1.780049)
geoip2_c                0.080000   0.010000   0.090000 (  0.078209)
bundle exec ruby bench.rb  13.26s user 0.83s system 99% cpu 14.134 total

geoip2_cはIPアドレスでlookupを実行しただけでは、実際の値を取得しません。他のライブラリはlookupの時点で値を取得しています。GeoIP2のライブラリでは取得する値が多ければ多いほど処理に時間がかかります。geoip2_cでもGeoIP2で利用できる値を全て取得すると処理に時間がかかるようになります。 利用可能な属性数は、表の通りです。

ライブラリ 利用可能な属性数
geoip 9
geoip2_compat 8
geoip2_c 7+17+(4*7)=52
hive_geoip2 7+17+(4*7)=52

geoip2_cでは例えば、以下のような属性を取得することができますが、実際のアプリケーションでは全ての属性を必要とすることは少ないでしょう。よって必要な属性を必要なときに取得するようにした方が効率がよいです。なお、GeoIP2ではIPアドレスによって取得できる属性に違いがあります。

{"city"=>{"geoname_id"=>10300919, "names"=>{"en"=>"Fort Huachuaca"}},
 "continent"=>
  {"code"=>"NA",
   "geoname_id"=>6255149,
   "names"=>
    {"de"=>"Nordamerika",
     "en"=>"North America",
     "es"=>"Norteamérica",
     "fr"=>"Amérique du Nord",
     "ja"=>"北アメリカ",
     "pt-BR"=>"América do Norte",
     "ru"=>"Северная Америка",
     "zh-CN"=>"北美洲"}},
 "country"=>
  {"geoname_id"=>6252001,
   "iso_code"=>"US",
   "names"=>
    {"de"=>"USA",
     "en"=>"United States",
     "es"=>"Estados Unidos",
     "fr"=>"États-Unis",
     "ja"=>"アメリカ合衆国",
     "pt-BR"=>"Estados Unidos",
     "ru"=>"США",
     "zh-CN"=>"美国"}},
 "location"=>
  {"accuracy_radius"=>1000,
   "latitude"=>31.5273,
   "longitude"=>-110.3607,
   "metro_code"=>789,
   "time_zone"=>"America/Phoenix"},
 "postal"=>{"code"=>"85613"},
 "registered_country"=>
  {"geoname_id"=>6252001,
   "iso_code"=>"US",
   "names"=>
    {"de"=>"USA",
     "en"=>"United States",
     "es"=>"Estados Unidos",
     "fr"=>"États-Unis",
     "ja"=>"アメリカ合衆国",
     "pt-BR"=>"Estados Unidos",
     "ru"=>"США",
     "zh-CN"=>"美国"}},
 "subdivisions"=>
  [{"geoname_id"=>5551752,
    "iso_code"=>"AZ",
    "names"=>
     {"de"=>"Arizona",
      "en"=>"Arizona",
      "es"=>"Arizona",
      "fr"=>"Arizona",
      "ja"=>"アリゾナ州",
      "pt-BR"=>"Arizona",
      "ru"=>"Аризона"}}]}

fluent-plugin-geoipのGeoIP2対応について

GeoIP2サポートする際、なるべくGeoIP Legacyと互換性を保つためにgeoip2_compatを利用し、GeoIP2で利用できる属性を全て使用するためにgeoip2_cを使用することにしました。 設定によってGeoIP Legacyも利用できるようにしました。

それぞれで利用できる属性は以下の通りです。

GeoIP Legacy:

placeholder attributes output example type note
${city[lookup_field]} "Ithaca" varchar(255) -
${latitude[lookup_field]} 42.4277992248535 decimal -
${longitude[lookup_field]} -76.4981994628906 decimal -
${country_code3[lookup_field]} "USA" varchar(3) -
${country_code[lookup_field]} "US" varchar(2) A two-character ISO 3166-1 country code
${country_name[lookup_field]} "United States" varchar(50) -
${dma_code[lookup_field]} 555 unsigned int only for US
${area_code[lookup_field]} 607 char(3) only for US
${region[lookup_field]} "NY" char(2) A two character ISO-3166-2 or FIPS 10-4 code

geoip2_c backend:

placeholder attributes output example note
${city.names.en[lookup_field]} "Mountain View" -
${location.latitude[lookup_field]} 37.419200000000004 -
${location.longitude[lookup_field]} -122.0574 -
${country.iso_code[lookup_field]} "US" -
${country.names.en[lookup_field]} "United States" -
${postal.code[lookup_field]} "94043" -
${subdivisions.0.iso_code[lookup_field]} "CA" -
${subdivisions.0.names.en[lookup_field]} "California" -

geoip2_cバックエンドでは、上記の属性だけでなくGeoIP2のデータベースに含まれる全ての属性を使用可能です。

geoip2_compat backend:

placeholder attributes output example note
${city[lookup_field]} "Mountain View" -
${latitude[lookup_field]} 37.419200000000004 -
${longitude[lookup_field]} -122.0574 -
${country_code[lookup_field]} "US" -
${country_name[lookup_field]} "United States" -
${postal_code[lookup_field]} "94043"
${region[lookup_field]} "CA" -
${region_name[lookup_field]} "California" -

geoip2_compatバックエンドでは、上記の属性のみ使用可能です。

geoip2_c/geoip2_compatを利用するにはlibmaxminddbを事前にインストールする必要があります*1

libmaxminddb*2は多くのLinuxディスリビューションでパッケージ化されていてそれぞれのパッケージマネージャで簡単にインストールすることができます。

GeoIP2を利用できるfluent-plugin-geoip 0.7.0がリリース済みです。互換性のためにgeoip2_cとgeoip2_compatはdevelopment dependenciesになっているので、GeoIP2を利用したい場合は、利用したいバックエンドに対応したGemを事前にgem installするかGemfileに記載してbundle installするかしてください。

なおFluentd v0.14 APIへの対応はこのプルリクエストで進行中です。

類似プロダクト

調査過程で見つけたGeoIP Legacy/GeoIP2に対応したfluent-pluginの比較を載せておきます。おすすめはもちろんfluent-plugin-geoipです。

GeoIP Legacy GeoIP2 速度 特徴
fluent-plugin-geoip 速い
fluent-plugin-filter-geoip × 遅い データベースを自動ダウンロードできる
fluent-plugin-geoip-filter × 速い LRUキャッシュ搭載
fluent-plugin-filter-geo × 遅い fluent-plugin-filter-geoipのfork

まとめ

fluent-plugin-geoipのGeoIP2対応を進めたときの流れをまとめてみました。

  1. 既存のライブラリが要件を満たしているかどうか調査した
  2. 既存のライブラリだと要件を満たせなさそうなので、自分で拡張ライブラリを書いた
  3. ベンチマークで性能を確認
  4. 実際にfluent-plugin-geoipに組み込んでプルリクエストを出した
  5. プルリクエストを出した後も、project ownerのy-kenさんやFluentd開発者のrepeatedlyさんと協力して改善しfluent-plugin-geoip 0.7.0をリリースしていただいた
  6. 今後はFluentd v0.14 API対応したfluent-plugin-geoipリリースを目指す

Rubyの拡張ライブラリは簡単に書ける*3ので今後も機会があればどんどん書きたいです。

*1 geoip2_compatはlibmaxminddbのソースコードをバンドルしているのでgeoip2_compatを利用する場合は事前にインストールしなくてもよい

*2 geoip2_cで必要なのは開発用のファイルなのでlibmaxminddb-devまたはlibmaxminddb-develをインストールする

*3 geoip2_cは最初FFIを使って実装するつもりでしたがstructやunionのalignmentを考慮するのが辛かったので止めました

タグ: Fluentd
2017-04-18

Fluentd v0.14で導入されたstorageプラグインとは

はじめに

Fluentd v0.14では新たにstorageプラグインという新しいタイプのプラグインが導入されました。 FluentdをインストールしただけではJSON形式により保存される storage_local プラグインしかありませんが、このstorageプラグインはFluentdのプラグインのインスタンスが保持する値をKVSに集約することにも使用することができます。

storageプラグインとstorageプラグインヘルパー

Fluentd v0.14ではさらにプラグインヘルパーという概念も追加されました。storageプラグインにおいてもstorageプラグインを直接使うのではなく、storageプラグインヘルパーを通じて使うことが推奨されます。

storageプラグインのAPI

storageプラグインは以下のAPIを持ちます。これはKVSから値を取り出したり、保存したり、また取り出した値をキャッシュしておくのに合うAPIとなっています。

# basically, interact with KVS
def load
end

# basically, interact with KVS
def save
end

# Normally, the following methods work as `cache`.
def get(key)
end

def fetch(key, defval)
end

def put(key, value)
end

def delete(key)
end

def update(key, &block) # transactional get-and-update
end

このうち、 #load#save については実際のKVSに対して値を読み込んできたり、保存したりする役割を担います。 一方、 #get#fetch#put#delete#updateについてはstorageプラグインだけではキャッシュとして振る舞うことが求められます。

storageプラグインヘルパー

storageプラグインヘルパーはstorageプラグインを直接使わずにstorageプラグインの性質を変化させるように作成されています。 storageプラグインヘルパーの #wrap_instance メソッドにより、storageプラグインのインスタンスをそのまま使用するか、storageプラグインの値を永続化して同期を取るか、単に同期を取るかが決定されます。

Fluentdの実際のコードでは以下のようになっています。

def wrap_instance(storage)
  if storage.persistent && storage.persistent_always?
    storage
  elsif storage.persistent
    PersistentWrapper.new(storage)
  elsif !storage.synchronized?
    SynchronizeWrapper.new(storage)
  else
    storage
  end
end

<storage> セクションに persistent true が設定されていることや、 #persistent_always? の返す値、#synchronized? が返す値が振る舞いを変えることがわかります。

実際のstorageプラグインの例

これらを踏まえて、筆者はMongo、Redis、Memchachedについてのstorageプラグインをそれぞれ作成しました。

これらを用いると上記3つのKVSに対してstorageプラグインによりownerプラグインであるinput, output, filterプラグインの情報をKVSへ集約することができます。

まとめ

Fluentd v0.14で導入されたstorageプラグインの概要とstorageプラグインヘルパーを通した場合の振る舞いの変化について解説しました。 storageプラグインをうまく活用するとstorageプラグイン対応が入っているfluent-plugin-systemdfluent-plugin-windows-eventlogのようにどこまで読んだかの位置の記録をKVSに集約することができるようになります。

タグ: Fluentd
2017-04-19

Firefoxのアドオンが認識されなくて困った時の確認方法

はじめに

Firefoxのアドオンを個人でインストールする場合、Firefoxのアドオンマネージャを経由するか、Add-onsサイトからインストールすることでしょう。

個人利用の場合にはそれでよいのですが、Firefoxを企業内に導入する場合には自由にアドオンをインストールさせないというポリシーを適用することがあります。 その場合、事前に許可したアドオンだけは使えるようにしたいという要件が必須となることがあります。

今回はFirefoxの企業内利用で事前に許可したアドオンだけは使えるようにしたいという要件を満たそうとしたものの、アドオンが認識されなくて困った場合の確認方法について紹介します。

Firefoxがアドオンを認識する場所

そもそも、アドオンはどこに配置することになっているのでしょうか。 アドオンがインストールされた状態としてFirefoxに認識されるパターンはいくつかあります。 Windowsの場合だと例えば以下です。

  • C:\Program Files (x86)\Mozilla Firefox\browser\extensions
  • C:\Program Files (x86)\Mozilla Firefox\distribution\extensions
  • %AppData%\Mozilla\Profiles\(プロファイル)\extensions (個人利用の場合はたいていこのケース)

基本は上記の3箇所です。企業利用において、 browser\extensions に配置するか distribution\extensions に配置するかはポリシーによって変わります。 通常は browser\extensions です。Firefox起動時にユーザープロファイルへとインストールさせたい場合にのみ distribution\extensions に配置します。

アドオンを配置したものの認識されない?

例えば、Add-onsのサイトからアドオンファイルのみダウンロードして、企業内利用のために配置する場合を考えてみましょう。 最新のアドオンはAdd-onsのサイトの「Firefoxへ追加」ボタンのリンクをたどることで個別にダウンロードすることが可能です。

一定期間後に履歴を削除するためのアドオン「Expire history by days」なら以下のリンクから入手可能です。

https://addons.mozilla.org/firefox/downloads/latest/expire-history-by-days/addon-331631-latest.xpi

しかし、xpiをそのまま browser\extensions へと配置してもFirefoxは認識しません。

実は、アドオンがFirefoxに認識されるかはある特定のファイル名になっているかによって決まります。 この決まりとは、アドオンのファイル名は em:id と呼ばれる値にならって命名するというものです。

では em:id は何を確認すればいいかというと、アドオンに含まれる install.rdf の中身をみるとわかります。

「Expire history by days」の場合、em:idem:id="expire-history-by-days@bonardo.net" となっています。 そのため、ダウンロードした addon-331631-latest.xpiexpire-history-by-days@bonardo.net にリネームして browser\extensions 以下に配置するとFirefoxにアドオンとして認識されるようになります。

まとめ

今回は、企業内利用で事前に許可したアドオンだけを使えるようにしようとして、アドオンがうまく認識されずに困った場合の確認方法を紹介しました。もしアドオンを所定の場所へと配置したのに認識されない場合には、ファイル名がアドオンの em:id と一致しているか確認してみてください。

2017-04-21

LibreOffice Calcのスプレッドシートの変更点をgit diffで見られるようにする

プログラマーは基本的にプレーンテキスト形式が好きな生き物で、ドキュメントならMarkdown、表形式のデータならCSVが定番です。プレーンテキスト形式だとシェルのコマンドや簡単なスクリプトで容易に加工できますし、Gitリポジトリなどに格納した状態でも変更点を追いやすいです。

しかし、たまにどうしても、もうちょっとリッチな形式のバイナリファイルをマスターデータとして持っておかないといけないことがあります。Microsoft ExcelやLibreOffice(OpenOffice.org)Calcなのスプレッドシートもそのひとつです。

Microsoft Excelの場合、悩みを抱える人が多いためか、すでに色々な方が解決策を公開されています。例えばGitで管理しているExcelファイルの差分を見るという記事では、Go言語製のツールを併用する手順が紹介されています。

一方、ODF(OpenDocument Format)のスプレッドシート形式(ods)についてはあまりそのような情報が出回っていないようです。odt2txtというツールを使ってドキュメント形式(odt)の差分を表示する方法の解説はあり、その一環でodsも差分を表示できるようになるのですが、元がodt用なのでodsについてはいまいち微妙な結果になってしまいます。

そこでこの記事では、「odsの変更点を差分表示する」という事に焦点を当てて解説することにします。

必要なのはファイル形式の変換

バイナリファイルの変更点の差分をgit diffで見られるようにするために必要なのは、要するに、diffで比較できる形式にファイルを変換するツールです。前述のExcel形式やodtの差分を見る方法も、それらのファイルに対応した*「引数で指定されたファイルをプレーンテキスト形式に変換して標準出力に出力する」というツールをいかに用意するか*がキモになっています。

odsをプレーンテキスト(CSV)に変換して標準出力に出力するコマンドが~/local/bin/ods2csvの位置に置かれていたとすると、Linux環境では

  1. コマンドをフィルタとして登録するため、~/.gitconfigに以下の行を追加する。

    # odfspreadheetというフィルタの実体として、ods2csvを登録する。
    [diff "spreadsheet"]
            textconv = ~/local/bin/ods2csv
    
  2. 拡張子とフィルタを対応付けるため、~/.gitattributesに以下の行を追加する。

    *.ods diff=spreadsheet
    
  3. 以下のコマンド列を実行し、~/.gitattributesをグローバルな設定ファイルとして登録する。

    $ git config --global core.attributesFile ~/.gitattributes
    

これで、手元のGitリポジトリ内で拡張子が.odsであるファイルに変更が加わっていた場合に、git diffを実行すると自動的にods2csvが実行され、プレーンテキストに変換した後の内容の差分が表示されるようになります。

ということで、あとはこのような振る舞いをするods2csvをどのように用意するかという話になります。

LibreOffice(OpenOffice.org)自体を使う

実は、LibreOffice(OpenOffice.org)はコマンドライン引数を使ってある程度の自動操作ができ、--convert-to csvと指定すれば、odsの中でアクティブなワークシートをCSVとしてエクスポートさせられます。以下は、この機能を使って~/local/bin/ods2csvをシェルスクリプトとして記述した例です。

#!/bin/bash
tempdir="$(mktemp -d)"
csvfile="$(basename "$1" .ods).csv"

# $HOMEを上書きしておかないと、すでにLibreOfficeのプロセスが起動している場合に
# ここでのsofficeの実行に失敗してしまう。
export HOME="$tempdir"
soffice --nofirststartwizard --headless --convert-to csv --infilter=CSV:44,34,76,1 --outdir "$tempdir" "$1"
cat "$tempdir/$csvfile"
rm -rf "$tempdir"

ただ、これだとあくまでアクティブなワークシート1つだけが変換されて他のワークシートは無視されてしまいます。複数ワークシートがあるファイルだと期待したような結果を得られません。

ssconvertを使う

より実用的な方法として、Gnumeric(Ubuntuであればsudo apt install gnumericでインストール可能)の一部として提供されているコマンドラインツールのssconvertを使う方法があります。

ssconvertはGnumericで取り扱える形式のファイルを変換する機能を提供していますが、出力形式をCSVにして、-S--export-file-per-sheet)オプションを指定すれば、すべてのワークシートを別々のCSVファイルに分割して出力させる事もできます。以下は、それを応用して~/local/bin/ods2csvをシェルスクリプトとして記述した例です。

#!/bin/bash

sscat() {
  ssconvert -S "$(basename "$1")" "$(basename "$1").%s.csv" 1>/dev/null 2>&1
  for csv in *.csv
  do
    echo "$csv:"
    cat "$csv"
  done
}

tempdir="$(mktemp -d)"
cp "$1" "$tempdir/"
(cd "$tempdir" &&
   sscat "$(basename "$1")")
rm -rf "$tempdir"

実際のところは、ssconvertは入出力ファイルの形式をファイル名から自動判別するようになっているため、このスクリプトは.xlsでも.xlsxでもGnumericが取り扱える形式なら何にでも使えます。よって、~/.gitattributes

*.ods diff=spreadsheet
*.xls diff=spreadsheet
*.xlsx diff=spreadsheet

と書いてしまって問題ありません。

まとめ

Gitリポジトリに格納された.odsの変更点を差分表示する方法として、ssconvertをベースにしたシェルスクリプトで.ods(およびその他のスプレッドシート形式のファイル)の全内容をCSVに変換して出力する方法をご紹介しました。

Gitでのバイナリファイルの取り扱いは何かと面倒ですが、差分が見やすくなるだけでも使い勝手はずいぶん向上するはずです。また、直接はGitのテキスト化フィルタに使えないコマンドでも、これらの例のようにシェルスクリプトを作成するだけでテキスト化フィルタにすることができます。お手元のリポジトリでスプレッドシート形式のファイルの取り扱いにお悩みの方は、ぜひ一度挑戦してみて下さい。

(なお、この方法はあくまでgit diffに対してのみ有効で、git loggit showに対しては機能しません。あしからずご了承ください。)

(なお、ここではgit diffについてのみ触れましたが、実際にはこの設定がなされていれば、git log -pgit showでもodsがCSVに変換された結果の差分が表示されるようになります。)

2017-04-24

Microsoft ExcelやLibreOfficeのスプレッドシートをフリーソフトウェアの法人向けサポート事業に活用する

クリアコードのフリーソフトウェア・OSSの法人向けサポートサービスでは、お客様向けの納品物としてExcel形式のワークシートや目視確認用の検証手順書を提供する場合が度々あります。この記事では、そういった納品ドキュメントの作成と更新の手間を軽減するために行っている工夫の一部をご紹介します。

ビジネス上必要となる「体裁の整ったドキュメント」

自由な気風を好むITエンジニアは、資料にはMarkdownやCSVのようにプレーンテキスト形式を好む場合が多いです。プレーンテキスト形式の資料には、

  • diffコマンドで簡単に差分を見られる。
  • Gitリポジトリなどに格納した時でも、コミットログで変更点を追いやすくなる。
  • スクリプト等で自動処理しやすい。

といった具合に、何かと取り扱いが容易であるという大きなメリットがあります。

しかし、それらのプレーンテキストベースの資料は、一般寄りの方にとってはあまりいい印象を持たれない場合があります。弊社の法人向けサポート事業においても、直接やりとりする情報システム部門の担当者の方はまだしも、その向こう側で決済等の意志決定を下される方々は、ITの専門家というわけではない事がままあります。そのため納品物としては、文章であればきちんと整形されたドキュメントが、表形式であればセルの色付けや結合などのレイアウト調整が施された物が望まれる傾向にあります。

その一方で、前述したようなメリットを享受しにくい事から、OSS開発者は整形されたドキュメントをできるかぎり一時ソースにはしたがらない印象もあります。

普段の作業効率を高める事と、納品物としての体裁を整える事。この両者をどうやって両立するかが、法人向けサポートでの地味に重要な点となります。

整形された文書はプレーンテキスト形式から自動生成できる

作業効率と納品物の品質の両立のための方法としてまず考えつくのは、ソースをプレーンテキスト形式で管理し、納品物を自動生成するというやり方です。

実際に、Pandocというツールを使うと、Markdown形式のソースからPDFやODF(odt)、Word形式(doc, docx)を自動生成したり、その際に目次を自動的に埋め込んだりといった事が容易にできます。これについての詳しい話は、Markdownで書いたテキストをPDFに変換して納品用ドキュメントを作成するという過去の記事をご覧下さい。

表形式のマスターデータはどうすればいい?

その一方で、表形式のドキュメントについて同じような事をやるのは大変です。表形式のデータのプレーンテキスト形式といえばCSVですが、CSVには行・列・セルに内容以外の属性情報を持たせることができません。また、セル内で改行するような複雑なデータや、セルの結合を含むような複雑な構成のデータだと、CSVのまま管理するのは骨が折れます。HTMLのtable要素やXML形式を使うとすると、そのような情報も問題なく保持できるようにはなりますが、今度は編集・閲覧が大変になります。このように、表形式のデータとは基本的にプレーンテキストの表現力では手に余るものだと言えます。

正攻法としては、必要なすべてのデータをRDBに格納しておき、Railsアプリケーションのようなインターフェースを介して編集・閲覧する方法が考えられます。この場合、入力・編集用のビューで入力しておき、何らかの方法で納品用のスプレッドシート形式のファイル(ods、xls、xlsxなど)を自動生成するということになります。この方法は柔軟性が高いですが、そのようなシステムの開発と維持にはそれなりのコストがかかるため、案件の性質によっては赤字になってしまいそうです。

システム開発よりは低コストで、完全人力よりは効率が良いスプレッドシートの利用

実際に弊社で手がけているFirefoxの法人向け導入支援や技術支援の場合、ほとんどのお客様は1年間は大きな仕様変更がないESR版を使われています。そのため資料の更新のための作業量も人力での作業で収まる規模です。変更点の調査やカスタマイズ結果の目視での検証など、システム開発では軽減できない部分のコストの方がはるかに大きいため、資料の作成部分だけをシステム化する動機は薄いというのが実情です。

とはいえ、資料の作成・更新を手作業だけで行うのにも手間はかかりますし、ヒューマンエラーによるミスも起こり得ます。容易に自動化できる部分だけでも自動化できればそれに越した事はありません。

システムを改めて開発するほどのコストはかけたくないが、ミスをしやすい部分は自動化したい。このような欲張りなニーズに対する答えとしては、スプレッドシート形式のファイルをマスターデータとして使うという方法が有効かもしれません。ods、xls、xlsxなどのスプレッドシート形式のファイルは、書式設定やセル結合などの高い表現力と、数式や関数による自動処理の機能を併せ持っています。また、これらの形式であればCSV形式への変換スクリプトを使って容易にプレーンテキスト化できるため、変更時の差分も確認しやすいです。

スプレッドシートとmustacheによる納品用ドキュメントの半自動生成

さて、ここからがこの記事の本題です。

相互に依存関係にあるカスタマイズ項目一覧表と検証手順書

前述のような事情から、クリアコードではFirefoxの法人向けカスタマイズ案件用の資料の一般化したバージョンについて、マスターデータをOpenDocument Formatのスプレッドシート(ods)として保持しており、各顧客向けに微調整したりExcel形式で保存し直したりして実際の業務に使用しています。

この資料は「カスタマイズ項目一覧表(カスタマイズメニュー)」と「検証手順書」が対になっており、それぞれ以下のようになっています。

  • カスタマイズ項目一覧表(configurations/customization-items.ods
    • 形式:OpenDocument Formatスプレッドシート(ods)。
    • 目的:法人での利用で頻出のカスタマイズについて「やりたい事」「その実現方法(設定内容)」を整理し、一覧で示す。
    • 選択された設定を検証するための検証手順書中の見出し番号を示す欄があり、その内容は検証手順書の最終的な内容に依存する。
  • 検証手順書(verification_manual/verification_manual.md
    • 形式:PDF(Markdown形式のプレーンテキストから自動生成)。
    • 目的:カスタマイズ項目一覧表の上で選択されたカスタマイズのための指定が期待通りに反映されているかどうか、また、それらの指定が期待通りの効果を発揮しているかどうかを目視検証する際の手順を示す。
    • 検証手順書で何を検証する必要があるかは、カスタマイズ項目一覧表で選択されたカスタマイズの内容に依存する。

この時、

  • カスタマイズ項目一覧表で選択された設定の一覧を、どうやって検証手順書に引き渡すのか?
  • 検証手順書で最終的に決定された見出しの番号を、どうやってカスタマイズ項目一覧表の「検証手順書対応番号」欄に引き渡すのか?

という事が問題になります。このドキュメントではこれらをどうやって解決しているのかを、順を追って説明していきます。

「カスタマイズ項目一覧表で選択された設定のリスト」の出力の自動化

カスタマイズ項目一覧表は、

  • Privacy-30は、ディスクキャッシュを制御する設定項目である。
    • Privacy-30-1が選択された場合、ディスクキャッシュは制御しない。
    • Privacy-30-2が選択された場合、ディスクキャッシュの最大サイズを制限する。
    • Privacy-30-3が選択された場合、ディスクキャッシュを無効化する。

という要領で「変更可能な箇所」と「取り得る選択肢」(およびその実現方法)が列挙されています。

この資料ですが、当初の運用方法は「静的なデータだけが記入された表を用意しておき、お客様向けに選択・推奨した設定をセルの色を変える事で示す」というものでした。つまり、スプレッドシートとは言いつつも、実際の所は「セルの結合と色付けができる静的な表」としてしか使っていませんでした。

ここから「どの設定が選択されたのか」という情報をそのまま出力するのは難しいです。というのも、odsやxlsxで使える関数には「あるセルの色を参照する」というような物が存在しないからです。仮にそれをダイレクトにやろうと思うと、Basicなどでマクロを書く必要があり、この方向に進むのは面倒ごとが増えるばかりになってしまいます。

こういう場面では、「セルの色という書式情報を、項目の選択状態を示すマスターデータにする」という発想を捨てれば話が単純になります。

  • 項目の選択状態を記入する専用の列を設ける。(これをマスターデータとする)
  • 選択状態を示す列に何か記入されていれ行を対象に、セルの色を変える。
  • 選択状態を示す列に何か記入されている行を対象に、選択された設定のIDを示す文字列を別のシートに出力する。

ExcelもLibreOfficeも「書式情報を参照する」ことは不得意ですが、「条件にマッチする行に自動的に書式設定を反映する」ことは得意です。それが「条件付き書式」という機能です。本題から外れるためここでは詳しくは述べませんが、実際のodsファイルをダウンロードしてLibreOfficeで開き、「書式」→「条件付き書式」→「管理」を選択すると、そのワークシートでどのような条件付き書式が設定されているかを見られますので、参考にしてみて下さい。 条件付き書式が機能している様子のスクリーンショット

選択状態を表す情報と書式情報とを切り離せれば、「選択された設定のIDを示す文字列を別のシートに出力する」という事も容易にできます。1番目のcustomization-itemsというシートを例に取ると、

  • A列は「選択状態記入用の列」として使い、ここは自由入力とする(何か記入されていれば真、そうでなければ偽)。
  • B列には数式を使って、A列に何か記入されている行だけに連番を振るようにする。
  • C列には数式を使って、項目のIDとなる文字列を自動的に組み立てるようにする。

という事をした上で、verify-targetsという名前のシートにVLOOKUP()関数を使った計算式を記入しておくことで、「customization-itemsのA列に何か記入されている選択項目について、そのIDを自動的に出力する」ということを実現しています(数式の詳細は、実際のファイルをご覧下さい)。 自動的に収集された「選択済み項目のID」のリストの様子のスクリーンショット

他のシートも同様の方法で選択項目のIDをverify-targetsに出力するようにしてあり、後はodsをCSVに変換する方法などを併用すれば、「どの設定が選択されたのか」という情報を簡単に取り出せます。また、この程度の数式であればODFでもExcelワークシートでも互換性があるため、odsをLibreOfficeで開いてxlsx形式で保存し直せば、そのままExcelワークシートとして管理し続ける事もできます。

「カスタマイズ項目一覧表で選択された設定」に基づく、検証手順書の内容の自動決定

検証手順書は、以下のような要領で検証手順が記載されたドキュメントです(実際にはPDFになります)。

4.4 攻撃サイトに対する警告

4.4.1 確認する項目

• Security-5-*
• Security-6-*
• Hide-1

4.4.2 準備

1. 前項に引き続き検証するか、または以下の状態を整えておく。
   1. カスタマイズ済み Firefox のインストールが完了した状態にする。

4.4.3検証

1. Firefox のロケーションバーに「 https://itisatrap.org/firefox/its-an-attack.html 」
   と入力し、 Enterを押下する。
   • 確認項目
     1. 攻撃サイトとしてブロックされない。 (Security-5-2)
2. Firefox のロケーションバーに「 http://itisatrap.org/firefox/unwanted.html 」と
   入力し、 Enter を押下する。
   • 確認項目
     1. 望ましくないソフトウェアの提供サイトとしてブロックされない。 (Security-5-2)
3. Firefox のロケーションバーに「 http://itisatrap.org/firefox/its-a-trap.html 」と
   入力し、 Enter を押下する。
   • 確認項目
     1. 詐欺サイトとしてブロックされない。 (Security-6-2)
4. 「ツール」→「オプション」を開く。
   • 確認項目
     1. 「セキュリティ」タブが表示されていない。 (Hide-1)

納品後にお客様側でも検証できるようするため、あるいは納品前の検証をこのように実施しましたというエビデンスにするためという目的の物なので、人が手作業で実施する前提の検証手順が記載されています。

上記の例は

  • Security-5-2 攻撃サイトに対する警告を行わない
  • Security-6-2 偽装サイトに対する警告を行わない
  • Hide-1 セキュリティタブを非表示にする

が選択された場合を想定した内容ですが、例えば「Security-5-1 攻撃サイトに対する警告を行う」が選択された場合には当然ながら期待される結果が変わりますし、「Hide-1」が選択されなかった場合には「4.」以下の手順は不要となります。

このように検証手順書の内容を細かく切り替えるための材料になるのが、前述のverify-targetsワークシートをCSV出力した結果です。

この検証手順書のソースはMarkdown形式ですが、mustacheという軽量テンプレートエンジンを使って以下のような書き方をしています。

## 攻撃サイトに対する警告

### 確認する項目

{{#Security-5}} - Security-5-\* {{/Security-5}}
{{#Security-6}} - Security-6-\* {{/Security-6}}
{{#Hide-1}} - Hide-1 {{/Hide-1}}

...

### 検証

{{#Security-5}}
1. Firefoxのロケーションバーに「 https://itisatrap.org/firefox/its-an-attack.html 」と入力し、Enterを押下する。
    - 確認項目
        1. 攻撃サイトとしてブロック{{#Security-5-1}}される。(Security-5-1){{/Security-5-1}}{{#Security-5-2}}されない。(Security-5-2){{/Security-5-2}}
1. Firefoxのロケーションバーに「 http://itisatrap.org/firefox/unwanted.html 」と入力し、Enterを押下する。
    - 確認項目
        1. 望ましくないソフトウェアの提供サイトとしてブロック{{#Security-5-1}}される。(Security-5-1){{/Security-5-1}}{{#Security-5-2}}されない。(Security-5-2){{/Security-5-2}}
{{/Security-5}}
...

mustacheでは{{#Security-5-2}}〜{{/Security-5-2}}のようなタグ風の書き方によって、その名前のパラメータ(ここではSecurity-5-2)の値が何か設定されている時だけそのタグで囲われた内容を出力する、という形で出力を切り替えることができます。これをMarkdownからPDFへの変換処理の中で前処理として行い、その際にverify-targetsワークシートをCSV出力した結果の内容をパラメータとして組み合わせることによって、「スプレッドシートの設定内容に基づいて検証手順書の内容を細かく切り替えた結果」をPDFに変換しています。

(ただ、実際にはmustacheの表現力はそれほど高くないです。そのため、mustacheによる処理を終えたコンテンツをさらに加工して、出力する必要がなくなった検証手順や節・章などを丸ごと削除してから、それをPDFに変換するようにしています。)

これらの事を実際にはどのようにやっているのか、という事についてはRakefileをご覧下さい。

検証手順書の見出し番号をカスタマイズ項目一覧表にフィードバックする

ここまでの処理を終えた段階で、手元には「設定が選択されたカスタマイズ項目表」と「選択されたカスタマイズ内容に対応する検証手順書」の2つができています。

しかしながら、これらのドキュメントの参照関係はあくまで一方通行です。検証手順書を眺めていて「この検証手順を実施すれば、カスタマイズ項目一覧のこの項目についての検証ができる」という事は読み取れるのですが、逆に、カスタマイズ項目一覧表を見ていて「この設定を検証する手順は、検証手順書のどこを見れば記載されているか」という事は読み取れないままです。

検証手順書の見出し番号が静的であれば、あらかじめカスタマイズ項目一覧に記入しておけるのですが、前述の通り検証手順書の章や節の数は選択された設定によって変動します。また、通しでの検証のしやすさを考慮した位置に検証項目を追加した場合にも、以降の章や節の番号がずれてしまいます。このような理由から、検証手順書の見出し番号は、最初からは記入しておけません。

そこで、先の検証手順書の生成時の前処理の一環として、「カスタマイズ項目のID」と「それに対応する検証手順書中の見出し番号」を列挙したCSVを出力するようにしています。カスタマイズ項目一覧表にはあらかじめこのCSVを入力として受け付けるための場所としてverify-targets-to-chaptersシートが用意されており、CSVファイルの内容を当該ワークシートに貼り付けると、他の各シートの対応する行に検証手順書の章番号・節番号が自動で流し込まれるようになっています。(CSVを自動的に参照するようにスプレッドシート内に数式を書いておくこともできますが、そうするとスプレッドシート単体での取り回しが悪くなってしまうので、今のところはCSVの内容を静的に貼り付けるという仕様にとどめています) PDF生成時の副産物をシートにフィードバックしている様子

以上の手順によって、納品用のスプレッドシートと検証手順書のPDFが手に入る事になります。スプレッドシートの体裁を多少変える程度の事はここまでで説明した自動化のプロセスには何ら影響を与えませんので、後は実際のお客様ごとの要件に合わせて、スプレッドシートをExcel形式に改めたり、表紙を足したりといった微調整を行うだけです。

まとめ

以上、スプレッドシートとMarkdown形式のファイルの2つをマスターデータとして、納品用のスプレッドシートと検証手順書のPDFを半自動生成する事例についてご紹介しました。

フリーソフトウェアやOSSの開発者にとっては、Excelなどのスプレッドシートは「非ITエンジニアの人達とやりとりするためだけの物」という感覚が強いかもしれません。しかし、数式や関数を活用すれば、大仰なシステム開発無しに十分な品質の納品物を少ない労力で作るための素材としても使えます。

皆さんもスプレッドシートの持つポテンシャルを引き出して、ぜひ業務を効率化しましょう!

2017-04-25

脆弱性を見つけるコードレビュー(OSコマンドインジェクション)

はじめに

日本では東京オリンピックに向けてサイバーセキュリティ強化の機運が高まっていますが、ソフトウェアの脆弱性を利用した攻撃は増加し続けています。 最近では、Apache Struts2やWordPressの脆弱性を利用した攻撃により、Webサイトの改ざんや情報漏えいなどが発生し、社会的に大きな影響を及ぼしています。

ソフトウェアの脆弱性は、(例えばバッファオーバーフローのように)バグとして見つかることもありますが、通常のユースケースでは問題にならない実装になっている場合もあり、コードレビューやテストをすり抜けてしまうことがあります。 脆弱性を発見するためには、どのような攻撃手法が存在するかを知る必要があり、発見にはそれなりの知見が必要となりますが、いくつかのチェックポイントがありますので、ここではそれを紹介してみようと思います。

ただ、あまり多くの脆弱性について言及すると、話題が発散してしまいますので、ここでは、OSコマンドインジェクションを防ぐためにコードレビューでチェックすべき点に絞って紹介します。

  • Pocke様より、本記事においてミスリーディングを誘う可能性がある箇所を以下のBlogにてご指摘いただきましたので、チェックポイント(3. 使用しているOSのコマンドを実行する関数がシェルを用いてコマンドを実行する関数なのかどうかをチェック)を1つ追加させていただきました。Pocke様、ご指摘ありがとうございます。 http://pocke.hatenablog.com/entry/2017/05/01/183053

概要

OSコマンドインジェクションとは、あるプログラムからOSのコマンドを実行する際に、ソフトウェアの作成者が意図しないコマンドをプログラムの利用者が実行できてしまう脆弱性のことです。これを利用すると例えば、作成者がls(Windowsの場合はdir)コマンドを呼び出してカレントディレクトリのファイルリストを取得することを想定した処理で、lsではなく、rm -rf *のような別のコマンドを実行できてしまいます。 この脆弱性は利用されると、OSで使用できる任意のコマンドをプログラム利用者が実行できてしまいますので、リモートからアクセス可能なプログラムにOSコマンドインジェクションの脆弱性が存在すると、不特定多数の利用者から、任意のコマンドを実行されてしまう状態に陥ることになります。

上記の通り、利用されると非常に危険な脆弱性ですが、幸いにしてコードレビューで注意すべき点はそれほど多くありません。 OSコマンドインジェクションの脆弱性を防ぐためにコードレビューでチェックすべきポイントは以下の通りです。

具体的なチェックポイント

  1. OSのコマンドを実行する関数を使用しているかどうかをチェックします。
  2. OSのコマンドを実行する関数に与える引数を、プログラム外部からの入力を使って構成しているかどうかをチェックします。
  3. 使用しているOSのコマンドを実行する関数がシェルを用いてコマンドを実行する関数なのかどうかをチェックします。
  4. OSのコマンドを実行する関数に与える引数が適切にエスケープされているかどうかをチェックします。

1.OSのコマンドを実行する関数を使用しているかどうかをチェック

OSのコマンドを実行する関数が使用されていなければ、OSコマンドインジェクションは引き起こせないため、これらの関数を使用しているか どうかが最初のチェックポイントとなります。 一例をあげると、PHPでは「system()」や「exec()」等、Perlでは、「eval()」や「open()」、「system()」等が該当します。 ここで挙げた例以外にも、OSコマンドインジェクションを引き起こす可能性のある関数がありますが、具体例の紹介は、別記事にて取り扱う予定です。

  • 使用していない場合 -> OSコマンドインジェクションの危険性はありませんので、以下のチェックは必要ありません。

  • 使用している場合 -> チェックポイント2へ進んでください。

2.OSのコマンドを実行する関数に与える引数を、プログラム外部からの入力を使って構成しているかどうかをチェック

OSのコマンドを実行する関数は、実行するコマンドを引数として渡します。この引数を組み立てる際にプログラム外部からの入力を使っている場合は、OSコマンドインジェクションが発生する可能性が高まります。

  • OSのコマンドを実行する関数に与える引数をプログラム外部からの入力を使わないで構成している場合 -> 以下のチェックは必要ありません。ただし、OSのコマンドの中には、xargsfind等の任意のコマンドを引数に指定出来るコマンドが存在するので、そのようなコマンドをOSのコマンドを実行する関数の引数にしている場合は修正が必要です。

  • OSのコマンドを実行する関数に与える引数がプログラム外部からの入力を使って構成している場合 -> チェックポイント3へ進んでください。

3.使用しているOSのコマンドを実行する関数がシェルを用いてコマンドを実行する関数なのかどうかをチェック

あるプログラムからOSのコマンドを実行する関数には、シェルを用いてOSのコマンドを実行する関数と、シェルを用いないで OSのコマンドを実行する関数があります。 OSコマンドインジェクションは、シェルが解釈可能な特殊文字等を利用し、作成者が意図しないコマンドも実行させるものですので、 問題となるのは、シェルを用いてOSのコマンドを実行する関数となります。

  • 使用している関数がシェルを用いないでOSのコマンドを実行している場合 -> OSコマンドインジェクションの危険性はないので、問題ありません。
    • ただし、シェルを用いないでOSのコマンドを実行する関数の中には引数の指定方法によって、シェルを用いる、用いないを切り替える関数がありますので、このチェックポイントでは、各関数のドキュメントをよく読むことをおすすめします。主要な言語での具体例については、改めて別記事にて紹介させていただきます。

    • また、以下のようにOSのコマンドを実行する関数に与える引数に直接外部からの入力を使っている場合は、外部から自由にOSのコマンドが実行できる状態ですので、シェルを用いる関数か用いない関数かにかかわらず外部からの入力を直接引数に渡さないように修正が必要です。

import sys
import subprocess

args = sys.argv
subprocess.call(args[1])
  • 使用している関数がシェルを用いてOSのコマンドを実行している場合 -> シェルを用いないでOSのコマンドを実行する関数を使用して下さい。
    • どうしてもシェルを用いないでOSのコマンドを実行する関数が使用できない場合、あるいは、使用している言語にそのような関数が用意されていない場合はチェックポイント4へ進んで下さい。
    • ただし、チェクポイント4の方法は確実に安全とは言い切れないため、可能な限りチェックポイント3の段階で対応して下さい。

4.OSのコマンドを実行する関数に与える引数が適切にエスケープされているかどうかをチェック

前述の通りOSコマンドインジェクションは、シェルが解釈可能な特殊文字等を利用し、作成者が意図しないコマンドも実行させるものです。 主要な言語では、OSコマンドインジェクションを防ぐためにシェルの特殊文字をエスケープする方法が存在します。 例えば、PHPでは「escapeshellarg()」という関数が文字列のエスケープのために用意されています。具体的なエスケープ方法に ついても、後日、別記事にて扱う予定です。 それらの方法を使用して、外部からの入力をエスケープしてから、OSのコマンドを実行する関数を呼び出しているかチェックします。

  • 外部からの入力をエスケープしてから、OSのコマンドを実行する関数を呼び出している

    • 可能であればOSのコマンドを実行する関数の使用をやめるか、OSのコマンドを実行する関数に渡す引数を 外部からの入力によって変更させないように修正するか、シェルを用いない関数に変更したほうが安全です。

      シェルの文字列解釈の仕様は複雑なため、各言語で用意しているエスケープの方法にもバグが存在することがあり、 正しくエスケープされない可能性があります。

  • 外部からの入力をエスケープせずに、OSのコマンドを実行する関数を呼び出している

    • OSコマンドインジェクションが実行可能ですので、ここまで紹介した方法で修正する必要があります。

終わりに

大まかに4つのチェックポイントを紹介しました。あまり難易度の高いチェック方法はなかったのではないかと思います。

本記事では、紹介できませんでしたが、OSコマンドインジェクションを引き起こす可能性のある具体的な関数や各言語で用意されている エスケープの具体的な方法については、また後日、改めて別記事として記載する予定です。

2017-04-27

Fluent Loggerの信頼性を高めるには

はじめに

Fluentdにログを送る方法として、Fluent Loggerを使う方法があります。 RubyやJavaにはそれぞれfluent-logger-rubyやfluent-logger-javaなどのFluent Loggerがあり、よくメンテナンスされています。 この記事ではFluent Loggerを使ってFluentd v0.12またはv0.14にログを送信する時にどのようにするとより確実にログ転送ができるようになるかを解説します。

最小構成のFluent_Loggerを作成するには では最小構成のFluent Loggerはどのような仕様に基づき実装されるべきかを解説しました。この記事はその続編です。

確実にログを送るには

確実にログを送るにはエラーが起きた時にそのエラーを回復する手段を提供されていることが必要です。

ログが送れたことをFluent Logger側で検出する

より確実にログを送信したことをLogger側で確認するにはOptionやResponse節にあるようにoptionを使う事が重要になります。 optionの中に128bitユニークなIDのbase64を取ったものをchunkをキーとしたペアに入れ、ackで返って来たbase64の値と比較してやる事で、Logger側で確実に送信されたものと判定出来ます。 *1 確実に送信されたログに関してはバッファから削除してしまって問題ありません。

TCP接続のエラーから回復して何度か再試行する

また、ネットワークの状況によっては、一回でTCPの接続を確立するのが難しく、何回か再試行する必要があることがあります。 このときに、接続を複数回繰り返す方法としては、一定期間ごとに試行する方法(periodic)、試行間隔を指数関数的に増やして行く方法(exponential back-off)のどちらかが取られます。

送信の再試行回数を超えてエラーとなった時のログ消失を防ぐ方法

アプリケーションが予期しない理由により停止してしまった場合に備えて、送信していないログをメモリ上のバッファに溜めておくだけでなく、ファイルに書き込む必要がある場合もあります。 このとき、ファイルにバッファを書き込む際にはFluentdのプラグインで扱いやすい形にしておくほうがよいです。 例えば、msgpackのバイナリ列のバッファをそのまま吐き出したり、TSV形式にすることで、in_tailにより送信ができなかったファイルを後から送信する、という回復処理が行えるようになっているとFluentdに長期間繋がらなかった際にログの消失を抑える有効な手立てとなります。

Fluent Loggerのよくある実装を踏まえての解説

ここからはFluent Loggerのよくある実装を踏まえて解説を行います。 これら3つの仕様をFluent Loggerに入れることができればより強固にFluentdへログを転送することが可能になるでしょう。

Require Ack Response

確実にログを送ったことをFluent Logger側で検出するのに実装するべきことは次の通りです。 Fluent protocolでは最後のoptionというフィールドに12byteのbase64エンコードされた値をchunkをキーとするKey-Valueを持たせることができます。*2 このオプションを用いることで、Fluentdへのログ送信が完了したことがFluent Logger側で確認できるようになります。

このオプションが実装されているFluent LoggerにはFluencyfluent-logger-nodeがあります。

送信の再試行

TCP接続のエラーから回復して何度か再試行する、ということを実現するには再試行の戦略を決める必要があります。 大きく分けてTCP接続に失敗した時に等間隔で再試行するか、それとも再試行の間隔を指数関数的に増やしていくかの二つの方法が取れます。 等間隔で再試行を行う場合は再試行の時間まであとどれくらいかを予測しやすくなりますが、一方で送信先のノードが落ちている場合は再試行回数が極端に増える結果となります。 そのため、最初は再試行間隔が最初は徐々に増やされ、だんだんと間隔が開いていく指数関数的に再試行時間を決める方法を筆者はとることが多いです。

送信がうまくいかなかった時のファイルへの出力

送信の再試行回数を超えてエラーとなった時のログ消失を防ぐにはどうしてもログが送信できない場合に、ローカルのストレージへファイルとして出力する方法が取れます。in_tailではmsgpackやTSV形式などでログファイルをパースすることが可能です。 どうしてもエラーの回復ができない場合には最終的にはFluent Loggerに渡したログをファイルに出力し、後日改めてFluentdのin_tailなどで送信エラーの起きたログを回収するようにすると良いでしょう。

おわりに

Fluent Loggerは単純にログを転送するだけではなく、Logger側でログがFluentdへ転送できたことを検知する仕組みを入れることができたり、TCP接続を確実に確立するための再試行の機構を取り入れたり、再試行回数の上限を超えてしまった時はファイルに転送しようとしたログをダンプする戦略が取れることを解説しました。 なお、この記事では解説できませんでしたが、この記事は筆者が作成したRustのFluent Loggerのfruentlyを作成するにあたって得られた知見を元にしています。

*1 より詳細には https://github.com/fluent/fluentd/wiki/Forward-Protocol-Specification-v1#option を参照すること。

*2 https://github.com/fluent/fluentd/wiki/Forward-Protocol-Specification-v1#grammar

タグ: Fluentd
2017-04-28

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|