説明
OGNL(Object-Graph Navigation Language)は、プロパティの参照に加え、メソッド呼び出し、演算子、コレクション、評価コンテキストに公開されたオブジェクトへのアクセスを扱う式言語です。Apache Strutsは値スタックなどの内部機能でOGNLを使います。
外部で制御できる文字列をOGNL式として解析・評価すると、攻撃者が意図しないオブジェクトやプロパティへアクセスする可能性があります。影響はルートオブジェクト、評価コンテキスト、許可されたクラス・メンバー、Strutsのセキュリティ設定に依存し、データの公開・変更からコード実行まで及ぶおそれがあります。
parseExpressionやcompileの結果も、信頼できない式のままです。構文解析が成功しても、認可や後の評価の安全性が保証されるわけではありません。
想定される影響
- データの公開・変更: 到達可能なオブジェクトグラフのプロパティを読み書きされるおそれがあります。
- セキュリティ判断の回避: 評価結果を認可、ルーティング、ポリシー判断に使うと、その判断を操作される可能性があります。
- コード実行: 危険なオブジェクト、メソッド、クラス、コンテキストへのアクセスを許す構成では、アプリケーションの権限でコードを実行されるおそれがあります。
- サービス拒否: 複雑な式や繰り返しの多い式によって、CPU、メモリ、オブジェクトアクセスが過剰に消費される可能性があります。
対処方法
式の文字列はアプリケーションが管理し、外部入力はデータとしてだけ渡してください。
- 確認済みの固定式だけを評価してください。リクエスト値を式や強制評価・二重評価の構文へ連結しないでください。
- ユーザーがフィールドや操作を選ぶ場合は、有限のサーバー管理の対応表で外部キーを定数式へ変換し、不明なキーを拒否してください。
- 信頼できない値は、ルートオブジェクト、評価コンテキスト、バインディング、プロパティのデータとして渡してください。
- 正規表現、エスケープ、文字除去、
sanitizeという名前のヘルパーだけで、任意のOGNL式を安全にできると考えないでください。英数字と下線だけに制限しても、サーバー側オブジェクトのプロパティ名を選ばれる問題は残ります。 - サポート中のApache Strutsと、Strutsが管理するOGNL依存関係を使ってください。以下のStruts 7.3.0とOGNL 3.4.12は版別の参考資料です。デプロイ時にはサポート状況とセキュリティ更新を確認してください。
例
変更前
変更前では、リクエストで受け取った文字列全体をOGNL式として評価します。
import jakarta.servlet.http.HttpServletRequest;
import ognl.Ognl;
import ognl.OgnlException;
final class VulnerableOgnlExample {
static Object readField(HttpServletRequest request, UserProfile profile)
throws OgnlException {
String requestExpression = request.getParameter("expression");
return Ognl.getValue(requestExpression, profile);
}
static final class UserProfile {
private final String displayName;
private final String email;
UserProfile(String displayName, String email) {
this.displayName = displayName;
this.email = email;
}
public String getDisplayName() {
return displayName;
}
public String getEmail() {
return email;
}
}
}
requestExpressionには単純なプロパティ名だけでなく、メソッド呼び出し、コンテキスト参照などのOGNL構文を指定できるため、攻撃者が評価する式そのものを選べます。
変更後
変更後では入力を限定された業務用キーとして扱い、サーバーが管理する定数式へ対応付けます。switch式を使えるJavaの例です。プロフィールと各項目へのアクセス権限、エラー応答は別途構成してください。
import jakarta.servlet.http.HttpServletRequest;
import ognl.Ognl;
import ognl.OgnlException;
final class SafeOgnlExample {
static Object readField(HttpServletRequest request, UserProfile profile)
throws OgnlException {
String requestedField = request.getParameter("field");
if (requestedField == null) {
throw new IllegalArgumentException("Missing field");
}
String fixedExpression = switch (requestedField) {
case "display-name" -> "displayName";
case "email" -> "email";
default -> throw new IllegalArgumentException("Unsupported field");
};
return Ognl.getValue(fixedExpression, profile);
}
static final class UserProfile {
private final String displayName;
private final String email;
UserProfile(String displayName, String email) {
this.displayName = displayName;
this.email = email;
}
public String getDisplayName() {
return displayName;
}
public String getEmail() {
return email;
}
}
}
2つの業務用キーだけを受け入れます。OGNLへ渡す文字列は常に、コード内のdisplayNameまたはemailリテラルです。
Apache Strutsの多層防御
次の制御は固定式を使う設計を補うもので、攻撃者が選んだ式を無害化するものではありません。
- クラス・パッケージの許可リスト
struts.allowlist.enableを有効にし、必要な項目だけを許可してください。Struts 7では既定で有効です。 struts.ognl.excludedNodeTypesでOGNL Guardを構成し、不要なASTノードの種類を禁止してください。- 可能であれば
struts.ognl.valueStackFallbackToContext=falseを使い、ActionContextへのフォールバックを無効にしてください。 struts.ognl.allowStaticFieldAccess=false、struts.disallowProxyObjectAccess=true、struts.disallowDefaultPackageAccess=true、struts.ognl.disallowCustomOgnlMap=trueなど、Struts 7の制限を緩めないでください。struts.ognl.expressionMaxLengthは必要な範囲で小さく保ってください。長さの制限は複雑さを抑えますが、式の意味を検証するものではありません。
Apache Strutsの文書では、-Dognl.security.managerによるサンドボックスはJDK 21以降では動作しません。セキュリティ境界として頼ったり、現在のJDK向けの対策として提示したりしないでください。
式の使用箇所の確認
ヘルパーやテンプレートが後から文字列を評価する場合も、式とデータを分離してください。認証・認可に使う評価結果と、ルートオブジェクトの公開範囲も確認してください。