タイトル/名前 | 更新者 | 更新日 |
---|---|---|
技術/Socketプログラミング/Java (Groovy) の Socketプログラミングを strace と Wireshark で覗く | msakamoto-sf | 2014-02-15 23:04:50 |
Java/Enumの使い方メモ | msakamoto-sf | 2014-02-11 19:32:43 |
日記/2014/02/11/Apache Commons FileUploadのCVE-2014-0050,JVN#14876762の確認(不完全燃焼) | msakamoto-sf | 2014-02-11 19:31:02 |
Java/Serializeメモ | msakamoto-sf | 2014-02-10 21:36:08 |
Ruby/rubygemsメモ("gem install"するその前にチェックしておきたい項目) | msakamoto-sf | 2014-02-10 18:31:46 |
Ruby/rbenv, ruby-buildメモ(2014-02版) | msakamoto-sf | 2014-02-10 18:04:03 |
Ruby/Bundlerメモ | msakamoto-sf | 2014-02-10 17:17:23 |
Ruby/Web開発メモ(Rack, Sinatra) | msakamoto-sf | 2014-02-10 17:02:22 |
技術/HTTP/Cookie, Set-Cookieヘッダにおける記号の扱いのバラつきについて | msakamoto-sf | 2014-02-08 21:21:16 |
日記/2014/02/03/JAX-RS 2.0, Jerseyのメモその2 | msakamoto-sf | 2014-02-03 08:15:32 |
JavaのSocketプログラミングでTCP通信を勉強し始めてから10年近く、ずっと気になっていた点として、socket周りのシステムコールとTCPパケットレベルでの挙動観察をしてみようと思います。
Java5から導入されたenumですが、うまく使いこなせるとより分かりやすいプログラムが書けます。
ヒントになりそうなサンプルコードを作ってみましたので、紹介します。(GitHubにTestNGテストコードの形式でアップしてます)
基本的な使い方:
EnumをキーとしたCollectionの紹介:
型安全(Type Safe)な定数定義としてのEnumと、その定数値からの逆引き:
enumの使い方はネット上にも色々資料がありますが、"Effective Java"の第二版に、基本からヒントになりそうな応用まで豊富に紹介されてますので、そちらもオススメです。
※内容的にはかなり浅くて緩い記事なので、参考程度の扱いでお願いします。また、以下はmsakamoto-sfのプライベートの活動になりますので、所属する組織の公式見解ではありませんのでご了承ください。
こんなのが出てまして、Apache Commons FileUpload については実際に使ったこともありましたので、ちょっと気になりました。
commons-fileuploadについては、 1.3.1 で修正されてるということなので、どんな修正されたのか?↑の参考資料中に、開発者自身のメーリングリスト上での発言がありました。
んー、なんかよく分かんないんですが、バッファサイズのチェックを強化したとかなんとか。
Content-Typeとかboundaryっぽい感じがしますので、関連RFCをざっと復習。
commons-fileupload:
あとよくわかんないのが、JPCERTの方のページに回避策として紹介されてる「Content-Type ヘッダのサイズを 4091 より小さいサイズに制限する」ですね。4091ってどっから出てきたのか謎です。
実際にSVNリポジトリ眺めてみると、r1565143 で、以下のコミットログでそれっぽい修正がされてます。
Fix CVE-2014-0050. Specially crafted input can trigger a DoS if the buffer used by the
<code>MultipartStream</code> is not big enough. When constructing <code>MultipartStream</code>
enforce the requirements for buffer size by throwing an <code>IllegalArgumentException</code>
if the requested buffer size is too small. This prevents the DoS.
このコミットでのメインとなる修正箇所は src/main/java/org/apache/commons/fileupload/MultipartStream.java のコンストラクタ部分です。
修正前:
public MultipartStream(InputStream input, byte[] boundary, int bufSize, ProgressNotifier pNotifier) { this.input = input; this.bufSize = bufSize; this.buffer = new byte[bufSize]; this.notifier = pNotifier; // We prepend CR/LF to the boundary to chop trailing CR/LF from // body-data tokens. this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length]; this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length; this.keepRegion = this.boundary.length; System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0, BOUNDARY_PREFIX.length); System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, boundary.length); head = 0; tail = 0; }
修正後:
public MultipartStream(InputStream input, byte[] boundary, int bufSize, ProgressNotifier pNotifier) { this.input = input; this.bufSize = bufSize; this.buffer = new byte[bufSize]; this.notifier = pNotifier; // We prepend CR/LF to the boundary to chop trailing CR/LF from // body-data tokens. this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length; if (bufSize < this.boundaryLength + 1) { throw new IllegalArgumentException( "The buffer size specified for the MultipartStream is too small"); } this.boundary = new byte[this.boundaryLength]; this.keepRegion = this.boundary.length; System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0, BOUNDARY_PREFIX.length); System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length, boundary.length); head = 0; tail = 0; }
bufSizeで渡されたバッファサイズが、boundaryLength+1より小さいと例外を投げるようになりました。
・・・この修正箇所が、DoSの発生箇所とイコールには思えませんでした。
深くコードを追ってる時間も無いので、もう直接 1.3 版と 1.3.1 版で実際に使って比べてみます。
とりあえず動けば良いので、Web上のサンプルを切り貼りしてこんなServlet作ってみました。
public class FileUploadSampleServlet extends HttpServlet { private static final long serialVersionUID = 7896611580723129112L; @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024 * 1024); ServletContext servletContext = this.getServletConfig() .getServletContext(); File repository = (File) servletContext .getAttribute("javax.servlet.context.tempdir"); factory.setRepository(repository); ServletFileUpload upload = new ServletFileUpload(factory); upload.setSizeMax(1024 * 1024 * 1024); try { List<FileItem> items = upload.parseRequest(req); Iterator<FileItem> iter = items.iterator(); while (iter.hasNext()) { FileItem item = iter.next(); if (item.isFormField()) { String name = item.getFieldName(); String value = item.getString(); System.out.println("[" + name + "]=[" + value + "]"); } else { String fieldName = item.getFieldName(); String fileName = item.getName(); String contentType = item.getContentType(); boolean isInMemory = item.isInMemory(); long sizeInBytes = item.getSize(); byte[] data = item.get(); System.out.println("fieldName=" + fieldName); System.out.println("fileName=" + fileName); System.out.println("contentType=" + contentType); System.out.println("isInMemory=" + isInMemory); System.out.println("sizeInBytes=" + sizeInBytes); System.out.println("length=" + data.length); } } } catch (FileUploadException e) { e.printStackTrace(); } resp.setContentType("text/html; charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("uploaded."); } }
はい。で、commons-fileuploadの1.3で、動かしてみます。こんなリクエストをBurpとかお好みのHTTP Proxyソフトで組み立てて送ってあげます。
POST /(サンプルServletのpath) HTTP/1.1 Host: xxxxx User-Agent: yyyyy Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Cache-Control: max-age=0 Content-Type: multipart/form-data; boundary=BOUNDARYABCABC Content-Length: 2909 --BOUNDARYABCABC Content-Disposition: form-data; name="t1" abc --BOUNDARYABCABC Content-Disposition: form-data; name="f1"; filename="corkboard.txt" Content-Type: text/plain abcdefg --BOUNDARYABCABC--
ちゃんと受信できれば正常動作確認OKです。
ここで 1.3 版で、"BOUNDARYABCABC" の部分を 4091 バイトの長さの文字列にして送信すると、これは正常に動きます。
ところが、4092 バイトの長さにして送ると、リクエストが返ってきません。
どうやら、org.apache.commons.fileupload.util.Streams#copy() の中のread系の処理で待ちが発生しているらしく、Threadが解放されません。何回も送りつけるとその分だけThreadが増えていってしまい、リソースが枯渇します。
これが 1.3.1 版になると、以下の様な例外が発生します。エラーとはなりますが、処理は進みますのでThreadは開放され、リソースが枯渇しなくなります。
org.apache.commons.fileupload.FileUploadBase$InvalidContentTypeException: The boundary specified in the Content-type header is too long at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:997) at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310) at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334) at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115) at net.glamenvseptzen.cve20140050.FileUploadSampleServlet.doPost(FileUploadSampleServlet.java:40) at javax.servlet.http.HttpServlet.service(HttpServlet.java:647) at javax.servlet.http.HttpServlet.service(HttpServlet.java:728) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99) at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:936) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1004) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603) at java.lang.Thread.run(Thread.java:722) Caused by: java.lang.IllegalArgumentException: The buffer size specified for the MultipartStream is too small at org.apache.commons.fileupload.MultipartStream.<init>(MultipartStream.java:338) at org.apache.commons.fileupload.MultipartStream.<init>(MultipartStream.java:367) at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:995) ... 22 more Caused by: java.lang.IllegalArgumentException: The buffer size specified for the MultipartStream is too small at org.apache.commons.fileupload.MultipartStream.<init>(MultipartStream.java:338) at org.apache.commons.fileupload.MultipartStream.<init>(MultipartStream.java:367) at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:995) at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310) at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334) at org.apache.commons.fileupload.servlet.ServletFileUpload.parseRequest(ServletFileUpload.java:115) at net.glamenvseptzen.cve20140050.FileUploadSampleServlet.doPost(FileUploadSampleServlet.java:40) at javax.servlet.http.HttpServlet.service(HttpServlet.java:647) at javax.servlet.http.HttpServlet.service(HttpServlet.java:728) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99) at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:936) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1004) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603) at java.lang.Thread.run(Thread.java:722)
さて、挙動の変化は確認できましたので、あとは「4091バイト」です。
※DoSが実際に発生する処理の特定までは今回はタイムアップなので諦めます。なので「不完全燃焼」です。
サンプルコードみたいに普通の流れでmultipartを処理するとき、MultipartStreamのコンストラクタは以下が使われました。
public MultipartStream(InputStream input, byte[] boundary) { this(input, boundary, DEFAULT_BUFSIZE, null); }
この "DEFAULT_BUFSIZE" というのが、4096 に設定されています。委譲されたコンストラクタの方では、boundaryLengthの計算で以下のコードを使ってます。
this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
BOUNDARY_PREFIXというのは、CR + LF + "--" の4バイトです。
つまり、4091というのはboundaryLengthが 4091 + 4 で 4095 になる境界値となるわけで、いかにも怪しそうです。
コードの深追いはタイムアップでやりたくないので、このへんで止めておきますが、バッファサイズ周りのズレが原因で、どこかでずっとread待ちになってしまい、処理が止まってしまったのが原因のようです。
そこで、コンストラクタの段階で不正なバッファサイズが指定されたら例外を発生させ、read待ちにならないような修正がなされたようです。
対策ですが、commons-fileuploadを単体で使っているプロジェクトであれば、1.3.1にアップデートするのが良いと思います。
もちろん、commons-fileuploadを使っていないプロジェクト or Tomcat7 以降(=Servlet 3.0以降) でも @MultipartConfig を使っていない箇所については対策はそもそも不要かなと・・・。まさか@MultipartConfigを使っていても使ってなくても動いてしまったりは・・・しないですよね・・・(未確認)。
アップデートできないなどの場合は、JPCERTの回避策にあるように、HTTPリクエストのContent-Typeをチェックしてboundaryが4091バイト以下になるよう制限を加えることになるでしょう。具体的にはリクエストのContent-Typeをチェックしてboundaryが4092バイト以上ならそこで異常終了させてFilterChainをそれ以上動かさない、ようなServletFilterを追加して、multipart処理を行うServletが動く前にそのFilterを通すように修正する、というのが思い浮かびました。
IP制限つけてログインセッションチェック必須の、社内専用ツールの場合などは、攻撃者となりうるのが著しく範囲が狭まるような状況なので、特に修正せずリスクを受け入れる、というような判断をするのもあり得ると思います。
DoS発生箇所の特定とロジックの解説まで踏み込めなかったので不完全燃焼ですが、今回は以上です。
JavaのSerializeについて勉強してみようと思い、簡単なサンプルコードで実験してみました。
https://github.com/msakamoto-sf/javasnack/tree/master/src/test/java/javasnack/testng1/ser
実務に使うとなると、デフォルトコンストラクタやtransientなど、色々注意すべき事項が多そうです。Effective Javaにも詳しい解説があるようですので、そちらをまず参考にしたいと思います。
他、参考:
Web開発で自分が前から気になってたのは、Tomcatのcluster構成で、セッション情報を共有する場合に、セッションに設定できるオブジェクトにはどんな制限があるか?でした。
Tomcatの公式ドキュメントを見ると、 "All your session attributes must implement java.io.Serializable" とありますので、やっぱりSerializeされて処理されてるんだと思いました。:
http://tomcat.apache.org/tomcat-6.0-doc/cluster-howto.html
rubygemsエコシステム:
実行環境を調べて、 本当に現在の環境のgemコマンドとインストール先で良いか確認を推奨。
$ gem environment
確認せずに、うっかり gem install するとこんなトラブルに遭遇します。
以下の点を十分吟味し、必要に応じて rbenv + ruby-build(Ruby/rbenv, ruby-buildメモ(2014-02版)) や Bundler (Ruby/Bundlerメモ) を導入して、適切な場所にインストールします。
GEM_HOME, RUBYLIB 環境変数の調整
※GEM_HOMEはgemの「インストール先ディレクトリ」の設定。GEM_PATHは、gemの「探索対象ディレクトリ」の設定。そのため、GEM_HOMEは一つしか設定しないが、GEM_PATHはOSのPATH分割文字列を使って複数のインストール先を指定できる、と理解してます。(2014-02現在の認識。間違ってたらすみません、指摘してもらえれば随時修正します)
お仕事でちょっとRubyを復習する必要がありました。ディストリビューションがパッケージでインストールするバージョンと衝突すると怖いのですが、2014-02現在ですと rbenv + ruby-build で分離してインストールする手法がメジャーかつ安定してるっぽいです。実際にCentOS6にインストールしてみて、触ってみました。
1. 開発ツール入れておく。あとGitも。
# yum groupinstall "Development Tools" # yum -y install git
2. OSX系の記事読むと、OpenSSLとreadlineの開発用パッケージをbrewでインストールしてる記事多かったので、念のため入れておく。
# yum -y install openssl-devel readline-devel
3. rbenvをGitHubからcloneする。(公式の通り)
$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
4. 公式にあるようなシェルスクリプト設定を入れ込む。今回は ~/.bashrc に以下の2行を追加でよかった。
export PATH="$HOME/.rbenv/bin:$PATH" eval "$(rbenv init -)"
5. ログインし直すなどして .bashrc の内容を反映させる。
6. ruby-buildをGitHubからcloneする。(公式の通り)
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
ruby-buildでインストールできるバージョン一覧を確認:
$ rbenv install -l
1.9系を入れてみる。
$ rbenv install 1.9.3-p484
2.0系を入れてみる。
$ rbenv install 2.0.0-p353
rbenv環境で使うバージョンを指定する。
$ rbenv global 1.9.3-p484 $ cat ~/.rbenv/version 1.9.3-p484
あるディレクトリでのみ、別のバージョンのRubyを使うようrbenvで設定する。これは "rbenv global" の設定を上書きする。
$ mkdir -p work/rbenv_r2.0 $ cd work/rbenv_r2.0 $ rbenv local 2.0.0-p353 $ cat .ruby-version 2.0.0-p353
現在のシェルで使用するRubyバージョンを設定する。これは "rbenv local" の設定を上書きする。
$ rbenv shell 1.9.3-p484
"rbenv shell" の設定を解除する。
$ rbenv shell --unset
rbenv, ruby-build参考:
rvm時代:
rvmからrbenv + ruby-buildへの乗り換え参考:
MacOSX前提:
Bundler:
アプリケーションが依存するライブラリとバージョンをファイルに記述して管理し、一括でインストールやアップデートを行えるようになる。
また、installコマンドで "--path" オプションを指定することで、システムワイドとは別にインストール先を分離することができる。例えば Rails3/4 それぞれに対応したアプリを同じマシン上で開発することも、"bundle install --path vendor/bundle" とすることでアプリごとに分離することができる。
実際の活用方法参考:
RubyでWebアプリを開発する時に、RackとかSinatraを使うためのメモ。
Rack:
2014-02-10時点での認識:(間違ってたらスミマセン、教えて頂ければ随時修正します)
RubyでWeb開発をする場合、Webサーバ側は幅広い選択肢がある。CGI, FCGI, WEBrick, Mongrelなど。
しかし、HTTPリクエストやレスポンスの処理はWebサーバごとに変わるため、Webサーバに依存した書き方をしてしまうとアプリケーションの移植性が損なわれてしまう。
そこで、Webサーバ間の差異を吸収し、一般的なレベルで抽象化して扱える、中間層としてRackが存在する。
Webアプリケーションや、WebアプリのフレームワークはRackが提供するクラスライブラリや抽象化レイヤーを扱うことで、RackがサポートしているWebサーバであれば移植性を確保できる。
(多分、現実は色々とエッジな処理をしようとするとそうも行かないんだろうけど、一般的なレベルではおおよそ上手く巻き取ってくれてるのでは。でなければここまでメジャーになることはないでしょうし・・・)
Sinatra:
シンプルなWebアプリフレームワークだが、静的ファイルのサポートやテンプレートエンジンもいくつかサポートしていて、小回りが効く。
お仕事の関係で、HTTPリクエストのCookieヘッダの値はどう解釈されているのか調べてみました。
通常はCookieヘッダの値部分は、空白以外の値で、一般的には余計な記号など入り込まないように言語やライブラリ、フレームワーク、あるいはプログラマが処理してます。
しかし、特殊な要件を満たすため、普段は設定しない記号系の文字もCookieで使う必要がありました。
そこでいくつかのメジャーなWebアプリ実行環境について、実際にCookieに特殊な記号を入れて送ってみて、Webアプリケーション側ではどのように処理されて、見えるようになるのか調べてみました。また、逆に、特殊な記号を入れた値をアプリケーションからCookieとして設定すると、最終的なSet-Cookieでどのようにエンコードやエスケープされるかも確認してみました。
2014-02-08時点での確認状況
言語/プラットフォーム | Cookieヘッダの解釈 | Set-Cookieでのエンコード |
---|---|---|
PHP5 | URLデコードする | setcookie():URLエンコード, setrawcookie():そのまま |
Tomcat7 | 一部記号を除きそのまま | 基本そのまま、一部記号が入ると""囲み |
Ruby(Rackベース) | URLデコードする | URLエンコードする |
ASP | (未検証) | (未検証) |
ASP.NET | (未検証) | (未検証) |
ASP.NET MVC | (未検証) | (未検証) |
Python(WSGIベース) | (未検証) | (未検証) |
node.js | (未検証) | (未検証) |
Play(Nettyベース) | (未検証) | (未検証) |
(その他) | (未検証) | (未検証) |
細かい状況については、以下の個別の詳細確認状況を御覧ください。
multipart アップロード:実装依存らしいが、メジャーなFWならそれぞれなりに対応している。
Security, CSRF対策関連:
CSRF対策についてだが、JavaScriptからヘッダーを付けるようにして、サーバ側でそのヘッダーの有無をチェックする方法がいくつかで紹介されていた。SOPに従えばヘッダーをJavaScriptで操作できるのはSameOriginに限定されるので問題ないだろう、とのこと。
ただ、これはXHR2が登場したため、前提は崩れていると思われる。また、XHR2におけるSOPは主にpreflightと、レスポンスを読めるか否かに関わってきており、リクエストを飛ばすだけであれば可能な状態になっていることは徳丸氏やはせがわようすけ氏からの各種スライド・講演資料などから確認できる。
・・・うーん・・・。
Jerseyでセッションを使うには・・・"@Context" でHttpServletRequestをInjectionするのが王道か。