swfobject.js がアレな話

 

もばいる全盛感のある世間的には今更,FLASH なんてどうでもいいし,swfobject.js 自体 2009年から更新されてないから,こんな古いものをと言われかねないような話ではあるものの,日本語での言及をあまり見てないし,つい先日もさる通信キャリアがトップページでやらかしてて多分知られてないんだろうなと思ったので書こうと思った次第.

swfobject.js とは何か

この記事の対象読者にとっては説明するまでもない話とはおもうけど一応前置きとして説明しておくと,swfobject.js は FLASH を web ページに埋め込むための JavaScript のライブラリ.クロスブラウザ対応してたり,面倒な HTML-tag のお作法を覚えなくても良くなったりとでデファクトスタンダードな感じのモノ.2007年とちょい古いがリクルートMTL の SWFObject v2.0 ドキュメント日本語訳や,AdobeSWFObject 2を用いたFlash Playerのバージョン検知とSWFファイルの埋め込みを読むと大体理解できるかと思う.

v2.x 系の前にv1.5 があってこっちもまだそれなりに使われてる.

何がマズイか

問題になる箇所は2ヶ所あるが,問題の根は同じ.

一つ目は,embedSWF() の flashVarsObj(,paraObj,attrObj) の扱い.もう一つは,ExpressInstall.

embedSWF()での flashVarsObj などの扱い

swfobject.js (v2.2) の embedSWF では flahvarsObj や parObj に設定されている値が HTML でエスケープされるべき値なのかどうかを関知しない.そのため,flashvarsObj に " を含む値を渡すと,Windows + IE 環境で,任意の HTML 要素挿入が可能になっている.SWFObject.js v2.2 でのサンプル

swfobject.js 1.5 でも同等の問題があり,addVariable() で " を含む値を渡すと全ブラウザで任意の要素が挿入できる.SWFObject.js v1.5 でのサンプル

ページの URL をFLASH に渡そうとして flashvars={'url':document.URL};swfobject.embedSWF(...,flashvars,...); としていたり,so.addVariable('referer', location.href ); としていたりして location.hash や location.search 経由で汚染されるのがよくあるパターン.これ自体は,swfobject のプロジェクトでも把握していて,FAQ の 9. How can I pass URIs or HTML code as a value using flashvars? に encodeURIComponent() を使ってエンコードしろと言っているように,ユーザ(ページ製作者)に一定の注意を促しては居る.ただし v2.x 系での話で v1.5 系で言及されてたかどうかはわからない.

HTML断片や URL を渡すからエンコードしなくちゃいけないのではなく,& と = が flashvars の特殊文字であり," が HTML の特殊文字であるために,それらを適切にエスケープしなければならないのだがこの FAQ のタイトルでは,そう言ってはないのでいつでも必要になる問題だということが見過ごされる可能性が高い.また,v2.2 において,flashvarsObj は paraObj.flashvars に変換されるだけの存在であり,実際に任意の HTML が挿入されるのは,paraObj から HTML 断片への変換処理にあることや,attrObj も同様に単に文字列連結しているだけで同様の危険性をはらんでいることからすると,flashvarsObj の扱いだけを注意すれば良いというものでもないこともこの FAQ から読み取るのは困難だと思う.

この話は,2008年にswfobject.js にチケット Issue 66: flashVars are not escaped (urlencoded) properly として登録され,長く議論されているが,swfobject.js 側が修正される可能性は殆ど無い.ライブラリ開発側としては,「そもそもswfobject.js を使わずとも flashvars 経由で URI を渡すならエンコードされてなければならないのだから先にエンコードすべき」「ライブラリ側で変換するようにすることですでにエンコードされている既存のコードに影響が出る」「encodeURIComponent が IE5.5+ を要求し,クロスブラウザ対応に制約が出る」と言う立場をとっている.限定的な場合に使える API があり,冒頭に挙げた Adobe 本家の資料でも最後に少しだけ言及しているが,後述するように余り役に立たない.

ExpressInstall

2つ目の問題は ExpressInstall 機能である.

swfobject.js の機能として FLASH の plugin はインストールされてるがバージョンが古い時に,その場でインストーラを呼び出してアップデートを促す ExpressInstall と呼ばれる機能がある.

これはいくつか細かい条件があるものの,要求するバージョンより古いバージョンがインストールされていると判定されると,内部で専用の FLASH の埋め込みコードを生成し,元の FLASH の代わりにインストーラを呼び出す FLASH を埋め込むことで実現している.この内部で生成している専用の FLASH の埋め込みコードに問題があり,一つ目の問題と同じ原因で v2.2 では IE で v1.5 ではほぼ全てのブラウザで 任意の HTML が挿入可能になっている.

v2.2 では,expressInstall.swf でインストールした後で戻ってくる URL (MMredirectUrl) を flashvars に設定するときに,URL (=window.location) を渡しているにもかかわらず,適切なエスケープをしていないために,location 経由の XSS を呼び込むことになっている.これ自体は問題として認識されている(Issue 172: ExpressInstall redirect not redirecting with all GET variable after updating Flash)が不完全なまま終了している.また,リダイレクトして戻ってくるページのタイトルを設定する MMdoctitle に document.title の先頭 47 文字をそのまま渡しているため,例えば検索結果の画面などで,ユーザ入力から TITLE 要素が作られる様なページで expressInstall が発動すると不具合が生じる可能性がある. SWFObject.js v2.2 での expressInstall のサンプルSWFObject.js v2.2 での expressInstall のサンプル2

v1.5 では,リダイレクトして戻ってくる URL が escape() してから設定されているため window.location を経由した XSS は起こらないが,v2.2 と同様に document.title を経由した問題が起きる可能性がある. SWFObject.js v1.5 での expressInstall のサンプル

対策

で,どうするかというと基本的にはエスケープをする以外にない.

一応,v2.x では,swobject.getQueryParamValue() を使うことで,埋め込んでいるページの document.location.search と document.location.hash で指定されているパラメータを「安全」に取り出せるようになっている.ただし取得元が document.location に限定されていて好き勝手な変数をエスケープできるようになっていない上,encodeURIComponent が無い環境ではいかなる変換もしないので,URL 全てを入れる場合や location 以外のところから取得したり,location を加工したりする場合,結局自力でやらなければならない.

  • SWFObject.js v1.5 はもうメンテされてないので使わない.使いたい場合は,addVariable や addParam に渡す値は適切にエスケープする.
  • SWFObject.js v2.2 を使う場合,
    1. flashvars を経由して値を渡す場合,& と = を flashvars のために," を IE での正常な HTML の生成のためにエスケープする.
    2. paraObj (PARAM 要素)や attObj (OBJECT要素の属性) 経由で渡す場合も " を IE での正常な HTML の生成のためにエスケープする.
    3. expressInstall を使わないように embedSWF の引数で xiSwfUrlStr に false になる値を設定する.
    4. expressInstall を使いたい場合,r427#331 の win.location を encodeURIComponent(win.location) か escape(win.location) かにする(参照. swfobject.jsの注意点(Google Code)). doc.title も同様エスケープする.どちらのエスケープもない環境はこの際切り捨てるか String.replace で頑張る.

SWFObject そのものは捨てて,jQuery SWFObject Plugin,(SWFObject | jQuery Plugins) を使うという方法もある.明示的にエスケープしないように指定されない限り内部でエスケープするように安全側に倒された実装になっている.

まとめ

SWFObject がエスケープされた値が渡されること期待した実装になっているのできちんとエスケープしないと XSS になるという話と,expressInstall が使われる場合にはエスケープされてない値で埋め込みコードが生成されるので,expressInstall が動作しないようにするか,パッチをあてて使うか,別のライブラリを使いましょうという話でした.

追記

一年たったので追記.

Github(https://github.com/swfobject/swfobject)にて2.3beta 版が公開中でまだ継続してく予定な様子.

上述の v2.2 までの問題はほぼ解消されている.が,かなりレアなケースで問題が起こすことが不可能ではないので doc.title もencodeURIComponent 掛けませんかとぷるり中.

追記2

プルリクエストがいつの間にか取り込まれてたのでちょっとだけ追記

この取り込まれた version で,ExpressInstall 時の MMdocTitle のエスケープが不十分なために MMredirectURL が上書きされてプラグインインストール後に任意のURL に飛ばされるというのが起こせなくなった.

vb.net から c# に書き換えたお話.

hoehoe2 の話の続きで,とくに実装の方の話.

変換

ShapeDevelop に Tween.sln を読み込ませて,Tween プロジェクトを右クリックして Convert -> From VB.Net to C# とやってハイおしまい.と行ければよかったのだけどもそんなことはなく色いろあるので対応.

  1. まず異常終了するので,変換後のプロジェクトが入るフォルダを見てどのファイルまで変換して落ちたのかを特定.
  2. ファイルを特定できたら上から半分を  #if 0 #endif で隠して変換.
  3. それでも落ちたら下半分を if-endif で隠して変換.と二分探索を繰り返してメソッド一つまで特定.
  4. ちなみに,落ちてたのはAppendSettingDialog.vb の 2466行目あたり,Twitter.vb の 3083 行目あたりと3143行目あたりなので目印つけてコメントアウトし変換.
  5. 異常終了はしないものの,変換できないファイルがあると,変換のログ出力に言われるのでそれの対応をする.
  6. ちなみに変換に失敗してたのは,Dynamic.vb の nullable, ImageDictionary.vb の Dictionary の From を使った初期化,Tween.vb での AndAlso/OrElse と LINQ の文末の判定処理,Spliter2.Panel2.Resize ハンドラ,Twitter.vb の Select Case の Is <> と言った箇所.続きは diff で
  7. これでめでたく C# になったので,Visual Studio で Tween.sln を開いてビルド出来る状態まで修正することになる.

ビルド出来るまで

めでたく逐語訳で VB.Net から C# になったものの,いくつか超えられない文法上の壁や ShapeDevelop 側のバグでビルド出来無いので対応する.変換器の異常終了への対応でコメントアウトした部分はここで手作業で変換した.

ディレクティブ

VB.Net だと #If 0/#End If#If UA = "True" Then/#End If とできるが C# だとできないので #if NOTDEFINED/#endif などで置き換える.

try-catch-finaly などで中括弧{}が挿入される箇所とディレクティブが重なるとうまく処理できないようで

Try
  [module] = assembly.DefineDynamicModule("Module")
Finally
#If ENABLE_LINQ_PARTIAL_TRUST Then
  PermissionSet.RevertAssert()
#End If
End Try

try{
  module = assembly.DefineDynbamicModule("Module");
#if ENABLE_LINQ_PARTIAL_TRUST 
}finally{
  PermissionSet.RevertAssert();
#endif
}

となってしまっていたので機械的に書き換えた.

Property

VB だと Property であっても,メソッドのように好き勝手に引数を付けて呼び出せるけども,C# だとそうは行かないので関数で置き換える.VB で引数のある無しで多重定義された Property もあるので適宜置き換えていく.SharpDevelop での変換でProperty の引数がなかったことにされているので,1つずつ確認していった.

配列

ary[idx] となるべきところがary(idx)と変換されないままになることがあるので,機械的に置き換えた.

LINQ

LINQ (query 式) の改行がなかったことにされたり,let や select がなかったことにされて意味が分からないことになってるので,vb 側のコードを見ながら置き換えた.

関数 ( ref/out )

C# だと関数の ref 引数にはデフォルトパラメータを与えられなかったり,property や null を ref に渡せなかったり,out にはちゃんと代入しないといけなかったり,値を返さないパスがあっちゃダメだったりと細かいところで VB が緩いので1つずつ潰していった.

Generic

変換器の制約で, Sub foo(Of int)foo<int>() となるべきところが ただの foo() にされていたので対応箇所を1つずつ確認していった.

その他

その他細かい文法上の違いで,interface に delegate を定義してたり(IHttpConnection.vb),内部クラスのそのまた内部 Interface を自分自身に適用させてたり(WebBrowserController.vb) と言った箇所を適当に変形させて,変換器が解釈をミスしまくってた Win32Api.vb をがっつり書き換えていった.

ビルド

このあたりで一応ビルドできるのでしてみると一応動作するものの関連 tweet をたどれなかったり,thumbnail が出なかったりで,時々例外を吐くのでそれを手がかりに変換ミスを探り,

VB だと field と同じように Property に初期値を 与えてるものがあって,変換の時にその処理が抜けていたのが原因だったので,プロパティの初期化をコンストラクタに追加して修正.

これでひとまず文法上はC# になったのでより C# らしく,Microsoft.VisualBasic.dll への参照を順次削除する作業にかかった.

より C# らしく

DateAndTime,Strings,vbConstants

DateAndTime は DateTime に,Strings の各関数は同等の機能に,vbConstants の CrLf は "\r\n" にと機械的に置き換えた.

イベント

まず VB.net ではコントロールのイベント処理(例:ボタンのクリックイベント)がこんな感じになる.

  1. Form1.Designer.vb で Private WithEvents button1 as Button としてボタンを宣言
  2. Form1.Designer.vb の InitializeComponent() で button1 = new Button()
  3. Form1.vb で Private Sub button1_click() Handles button1.click として処理内容を書く

これに対して,C# だと

  1. Form1.Designer.cs で private Button button1; と宣言
  2. Form1.Designer.cs  の InitializeComponent() で button1 = new Button(); して button1.click += button1_click;
  3. Form1.cs で private void button1_click() で処理内容を書く

となる.通常のフォームのコントロールであれば InitializeComponent() で一回作ればフォームが死ぬまで作り変えられることはないのでイベントハンドラの追加も一回あればよく,C# で明示的にハンドラを追加していることに特に問題はない.逆に言うと,C# では new して作ったオブジェクトには明示的にハンドラを追加してやらないと問題がでるということである.これを解決するために ShapeDevelop が取った方法が,プロパティの set{} に隠蔽するという方法で,汎用性を考えると悪くない方法なのだが,Control には周りくどい上デザイナでうまく扱えないので InitializeComponent() で追加するように全て置き換えた.

この変換に関して VB で WithEvents が付いていて同時に初期化もされるフィールドを,C# で フィールドとプロパティにしたときにフィールドの初期化はするものの,プロパティの set {} を経由しないのでイベントハンドラが割当てられないという SharpDevelop の変換処理の不具合があって,タイマー処理がされなくなっていたのでタイマーの new 直後にイベントハンドラの追加コードを追加して修正した.

Exit For

VB だと割りと自由にブロックを抜け出せる.

例えば,

For Each Ele In Elements
  Select Case Ele.attr
     Case AttrA
       foo()
       Exit For
  End Select
Next
bar()

とすれば Ele.attr が AttrA のとき bar() に制御を移せるが,C# でこれを実現する文法上の方法はないので

bool flg = false;
foreach( var Ele in Elements ){
  switch(Ele.attr){
    case AttrA: 
      foo(); 
      flg = true;
      break;
  }
  if ( flg ) { break; }
}
bar();

と一手間掛ける必要がある.

幸いにも,変換処理でこの手の Exit にはすべて TODO: が付けられているので 1つずつ適切に変換することが出来た.

Static Local

VB にだけある機能として Sub で Static な変数を定義できる.Sub が呼び出されるても値が初期化されず残る C や C++ の関数の static 変数と同じ機能がある.ところが,C# の文法にはないので,クラスのフィールドと Microsoft.VisualBasic.CompilerServices.StaticLocalInitFlag を使うように変換される.ところが,実際にはフィールドにするくらいで十分(だろう)と判断してさっくりと削除した.

My

VB のアプリの最大の特徴とも言えるのがこの My で,この My の下に Settings や Resources があり,その他にすべてのフォームのインスタンスやApplication,キーボード,オーディオとあらゆるものが詰め込まれていてほとんどどこからでも呼び出せる.C# だと Settings や Resources は普通 Properties 以下に配置され,Application ではなく Assembly.GetExecutingAssembly を経由する.

名前空間は置き換えれば済んだし,その他も同等の機能を実装して対応した.

My.Forms を経由した別フォームへのアクセスも実際には TweenMain (form) が Owner として設定されているのでそれを経由することで対応した.

最後まで残ったのは MyApplication である.

MyApplication

MyApplication とは何かというのは,そもそもアプリがどう起動するかという話で,面倒なので http://www.atmarkit.co.jp/fdotnet/dotnettips/631vbappfx/vbappfx.html でも読んでもらうとして,結局は Main にベタに書けば済むことなので applicationEvents.vb から Program.cs に移してめでたく終了.

 

My や MyApplication の理解があやふやで, 実際 Culture 周りが残ったままなので ,これじゃマズイのかもしれないけどもひとまずの記録として.

 

hoehoe2

t-ashula / hoehoe2 てな形で,Tween ver. 1.1.0.0 GPLv3 版から fork して C# 化の区切りがついたのでエントリ起こし.

opentween ていう風にプロジェクトを構えて運用していくほどの気力はなくて,今のところは単純に自分用. twitter クライアントとしてどうこうというより  .NET と mono,C# と VB.net の互換性みたいな技術的な題材として見ている面が強い.

 実際公開はしたけれ,ライセンス的な問題,API キーの問題,多言語化,インストーラプロジェクトの変更,アップデート周りの整備と自分一人で使うには問題ないけど tween の oss fork プロジェクトとして野に放つには抱えてる問題は多い.なので,これまで維持してきた@kiri_feather たちはすげぇなと思うわけです.

で, 以下幾つかの抱えてる問題と技術的なお話.

issue

ライセンス的な問題

もともと v1.1.0.0 までは GPLv3 が適用されていたので,ココから GPLv3 に基づいて fork する分にはなんの問題もない.が,問題は幾つかのアイコン類のどこまでが GPLv3 の適用範囲になるかというところと Dynamic.vb

アイコン類

アイコン類は実際には大した問題じゃない.

Tween v1.1.0.0 に同梱のLICENSE.ja.txt に,Tween/Resources ディレクトリに含まれるアイコンファイルは GPL でライセンスされないと書かれているが, これらのファイルがオブジェクトコード ( Tween.exe ) の生成に必要な 対応するソースコードである以上 GPL と互換性のないライセンスで頒布することは出来無いはず.

なので,強行的に GPL とみなして使い続けてもライセンス上の問題はないと思う.

が,そんな微妙な状態のまま使うより,アイコンなので Tween と別物であるという意味も含めてライセンス的に問題ないものに置き換えて行く予定.

Dynamic.vb

 内部で使われている Dynamic.vb  (取得元 ) が MS-PL でライセンスされている.

FSFOSD 適合ライセンスだとしながらも, MSPL と GPL は 非互換との立場 をとっている.

ソースコードでの頒布時に MSPL にしないとダメだからというのが理由のようだが,(cf. http://lwn.net/Articles/254717/ ) 今ひとつはっきりしていない.

個人的には,ライセンス混在とか面倒なので正直避けたいのでいずれ置き換える予定.

API キーの問題

オリジナルの Tween が対応していた幾つかの Web サービスの API キーについては置き換える必要がある.ソースのどこにそのキーが書かれているかとか,どこに問い合わせればキーが得られるかは文書化されてないので,1つずつ調べて置き換えていくしかない.別に自分が使ってないサービスならどうでもいいかと思わんでもないので正直しんどい.

これを書いてる時点では,Twitter の ConsumerKey と ConsumerSecret 以外は置き換えてない. 

多言語化

オリジナルの Tween  だと英語と中国語のリソースがあって切り替えられるようになっていたのだけども,VB から C# に変換するときに,バッサリと切り替え周りの処理を切ってしまったのと,検証用の環境が無いので無用の長物になってるんじゃないかな.個人用なら別に問題ないので予定は未定. 

インストーラ

InstallShield をつかったプロジェクトがオリジナルのソリューションにはあって,個人用なら別にインストーラ要らないし,InstallShield 有料だし,WiX とかで置き換えるのも技術的には面白いかもねとかあって決めかねてる.

アップデート周り

 オリジナルの Tween の SourceForge.jp のプロジェクトの URL を参照してるのでこのへん削除するなり,置き換えるなりが必要.

 TweenUp.exe の方も変換するなり削除するなりやらないとマズイ. 

 個人用なら別に(以下略 

その他細かい技術的なことを言うと,

  1. テストコード無い
  2. 命名法がない
  3. 確たるコーディングスタイルがない
  4. WinForms べったり.せめて DataBinding を.
  5. etc

という話があったりなかったりで誰が幸せになるのか分からないけど,まあ楽しいからいいや.