JPCERT コーディネーションセンター

IDS08-J. 信頼できないデータは regex に渡す前に無害化する

正規表現は文字列のパターンマッチングに広く使われている。たとえば POSIX の grep ユーティリティでは、指定されたテキストの中から一定のパターンを拾い出すために、正規表現を利用できる。正規表現について詳しくは、Java チュートリアルを参照[Tutorials 08]。 java.util.regex パッケージは、コンパイル済みの正規表現を保持する Pattern クラスと、正規表現を解釈して CharSequence に対するパターンマッチングを行う Matcher クラスを提供する。

Java の強力な正規表現(regex)関連の機能は、悪用されないように保護しなければならない。攻撃者が元の正規表現を書き換えるような入力を与えることで、書き換えられた正規表現がプログラムの仕様に沿わなくなる可能性がある。この攻撃手法(正規表現インジェクションregex injection)によって、プログラムの実行フローの改変、情報漏えい、サービス運用妨害(DoS)などが発生するおそれがある。

Java の正規表現に関する以下の機能や性質は攻撃に使われやすい。

正規表現インジェクションを避けるために、信頼できない入力は必ず無害化してから使うべきである。ユーザが正規表現を直接入力する場合は、元の正規表現がむやみに改変されないように注意しなければならない。正規表現パーザに渡す前に、ユーザ入力文字列に含まれる文字を(たとえば文字や数字のみ、などに)ホワイトリストで無害化するのは優れたアプローチである。ユーザに提供する正規表現の機能はできるかぎり制限し、悪用の機会を最小化すること。

正規表現インジェクションの例

1つのシステムログファイルに様々なシステムプロセスから出力されたメッセージが含まれていると仮定する。パブリックなメッセージを出力するプロセスもあれば、"private"の印が付いたセンシティブな情報を含むメッセージを出力するプロセスもある。以下にそのようなログファイルの例を示す。

4/8/11 10:47:03 AM	private[423] Successful logout  name: somename ssn: 111223333
4/8/11 10:47:04 AM	public[48964]	Failed to resolve network service using name = Scipio type = _afpovertcp._tcp domain = local.
4/8/11 10:47:04 AM	public[1]	(public.message[49367]) Exited with exit code: 255
4/8/11 10:47:43 AM	private[423] Successful login  name: somename_else ssn: 444556666
4/8/11 10:48:08 AM	public[48964]	Backup failed with error: 19

ユーザはログファイルの中から興味のあるメッセージを検索したいと考えているが、"private" のしるしが付いたものは検索できないように制限されているとする。プログラムは、検索したいテキストパターンをユーザから受け取り、以下のような正規表現を組み立てるかもしれない。

(.*? +public\[\d+\] +.*<SEARCHTEXT>.*)

しかし、攻撃者が <SEARCHTEXT> の部分にどんな文字列でも入れられるとすると、以下に示すような文字列を入力することでインジェクション攻撃が可能になる。

.*)|(.

これを埋め込んだ正規表現は以下のようになる。

(.*? +public\[\d+\] +.*.*)|(.*.*)

この正規表現は、"private"のしるしが付いている行を含むすべての行にマッチする。

違反コード

以下の違反コード例では、ログファイルを定期的にメモリに読み込み、キーワードを引数に受け取った suggestSearches() はログファイルのキーワード検索結果を返している。

public class Keywords {
  private static ScheduledExecutorService scheduler
      = Executors.newSingleThreadScheduledExecutor();
  private static CharBuffer log;
  private static final Object lock = new Object();

  // ログファイルをメモリにマップする、さらに定期的に読み込み直す
  static {
    try {
      FileChannel channel = new FileInputStream(
          "path").getChannel();

      // ファイルサイズを調べ、ファイルをメモリにマップする
      int size = (int) channel.size();
      final MappedByteBuffer mappedBuffer = channel.map(
          FileChannel.MapMode.READ_ONLY, 0, size);

      Charset charset = Charset.forName("ISO-8859-15");
      final CharsetDecoder decoder = charset.newDecoder();

      log = decoder.decode(mappedBuffer); // ファイルをバッファに読み込む

      Runnable periodicLogRead = new Runnable() {
        @Override public void run() {
          synchronized (lock) { 
            try {
              log = decoder.decode(mappedBuffer);
            } catch (CharacterCodingException e) {
              // 例外ハンドラへ処理を移す
            } 
          }
        }
      };
      scheduler.scheduleAtFixedRate(periodicLogRead, 0, 5, TimeUnit.SECONDS);
    } catch (Throwable t) {
      // 例外ハンドラへ処理を移す
    }
  }

  public static Set suggestSearches(String search) {
    synchronized (lock) {
      Set searches = new HashSet();

      // ユーザ入力文字列から正規表現を組み立てる
      String regex = "(.*? +public\\[\\d+\\] +.*" + search + ".*)";
  
      Pattern keywordPattern = Pattern.compile(regex);
      Matcher logMatcher = keywordPattern.matcher(log);
      while (logMatcher.find()) {
        String found = logMatcher.group(1);
        searches.add(found);
      }
      return searches;
    }  
  }

}

このコードが実現しているのは、信頼できるユーザが public なログメッセージから "error" を含むものを検索することを許すものだが、すでに説明したように、攻撃者は正規表現インジェクションが可能である。

適合コード (ホワイトリスト化)

以下のコードでは、英数字以外の文字(ただし空白文字とシングルクオートを除く)を削除することで、正規表現インジェクションを防いでいる。

public class Keywords {
  // ...
  public static Set suggestSearches(String search) {
    synchronized (lock) {
      Set searches = new HashSet();

      StringBuilder sb = new StringBuilder(search.length());
      for (int i = 0; i < search.length(); ++i) {
        char ch = search.charAt(i);
        if (Character.isLetterOrDigit(ch) ||
            ch == ' ' ||
            ch == '\'') {
          sb.append(ch);
        }
      }
      search = sb.toString();

      // ユーザ入力文字列から正規表現を組み立てる
      String regex = "(.*? +public\\[\\d+\\] +.*" + search + ".*)";
      // ...
    }
  }
}

このコードは正当な検索パターンも制限してしまっている。たとえば、"name =" という文字列での検索はできない。'=' という文字は無害化によって正規表現から削除されてしまうからだ。

適合コード

別の対策方法として、マッチングを行う前にセンシティブな情報を削除しておくというやり方もある。しかしこの方法では、ログファイルを読み込み直す度にセンシティブな情報の削除が必要であり、コードが複雑になり、パフォーマンスの悪化を招く。また、ログエントリの形式が変更されてもコードがそれに合わせて修正されないと、依然として情報漏えいにつながるおそれがある。

リスク評価

このルールの違反は情報漏えいにつながる可能性がある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
IDS08-J P4 L3
関連ガイドライン
MITRE CWECWE-625, "Permissive Regular Expression"
参考文献
[Tutorials 08]Regular Expressions
[CVE 05]CVE-2005-1949
翻訳元

これは以下のページを翻訳したものです。

IDS08-J. Sanitize untrusted data passed to a regex (revision 78)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter