IDS08-J. 信頼できないデータは、正規表現に含める前に無害化する
正規表現(regex)は文字列のパターンマッチングに広く使われている。たとえばPOSIXのgrep
ユーティリティでは、指定されたテキストの中から一定のパターンを拾いだすために、正規表現を利用できる。正規表現について詳しくは、Javaチュートリアルを参照 [Java Tutorials]。java.util.regex
パッケージは、コンパイル済みの正規表現を保持するPattern
クラスと、それを使ってCharSequence
に対するパターンマッチングを行うMatcher
クラスを提供する。
Javaの強力な正規表現関連の機能は、悪用されないように保護しなければならない。攻撃者が元の正規表現を書き換えるような入力を与えることで、書き換えられた正規表現がプログラムの仕様に沿わなくなる可能性がある。この攻撃手法は正規表現インジェクション(regex injection)と呼ばれ、プログラムの実行フローの改変、情報漏えい、サービス運用妨害(DoS)などを発生させるおそれがある。
Java の正規表現に関する以下の機能や性質は攻撃に使われやすい。
- マッチフラグ: 信頼できない入力によって
Pattern.compile()
メソッドに渡された(あるいは渡される前の)マッチフラグが改変される - Greediness: 信頼できない入力によって、意図したよりも広い範囲にマッチするよう元の正規表現が改変され、センシティブな情報の漏えいにつながる
- グループ化: 正規表現の一部を丸かっこでくくることにより、マッチする部分にまとめて(グループ化)処理を行うことができる。信頼できない入力によりグループ化する部分が改変される
正規表現インジェクションを避けるために、信頼できない入力は必ず無害化してから使うべきである。ユーザが正規表現を直接入力する場合は、元の正規表現がむやみに改変されないように注意しなければならない。正規表現パーザに渡す前に、ユーザ入力文字列に含まれる文字を(たとえば文字や数字のみ、などに)ホワイトリストで無害化するのは優れたアプローチである。ユーザに提供する正規表現の機能はできるかぎり制限し、悪用の機会を最小化すること。
正規表現インジェクションの例
1つのシステムログファイルに様々なシステムプロセスから出力されたメッセージが含まれていると仮定する。パブリックなメッセージを出力するプロセスもあれば、「private」の印が付いたセンシティブな情報を含むメッセージを出力するプロセスもある。以下にそのようなログファイルの例を示す。
10:47:03 private[423] Successful logout name: usr1 ssn: 111223333
10:47:04 public[48964] Failed to resolve network service
10:47:04 public[1] (public.message[49367]) Exited with exit code: 255
10:47:43 private[423] Successful login name: usr2 ssn: 444556666
10:48:08 public[48964] Backup failed with error: 19
ユーザはログファイルの中から興味のあるメッセージを検索したいと考えているが、「private」の印が付いたものを見せてはいけないものとする。プログラムは、検索したいテキストパターンをユーザから受け取り、以下のような正規表現を組み立てるかもしれない。
(.*? +public\[\d+\] +.*<SEARCHTEXT>.*)
しかし、攻撃者が<SEARCHTEXT>
の部分にどんな文字列でも入れられるとすると、以下に示すような文字列を入力することでインジェクション攻撃が可能になる。
.*)|(.*
これを埋め込んだ正規表現は以下のようになる。
(.*? +public\[\d+\] +.*.*)|(.*.*)
この正規表現は、「private」の印が付いている行を含むすべての行にマッチする。
違反コード
以下の違反コード例では、信頼できないユーザから検索パターンを受け取り、ログファイルを検索している。
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LogSearch {
public static void FindLogEntry(String search) {
// ユーザ入力文字列から正規表現を組み立てる
String regex = "(.*? +public\\[\\d+\\] +.*" + search + ".*)";
Pattern searchPattern = Pattern.compile(regex);
try (FileInputStream fis = new FileInputStream("log.txt")) {
FileChannel channel = fis.getChannel();
// ファイルサイズを調べ、ファイルをメモリにマップする
long size = 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();
// ファイルをバッファに読み込む
CharBuffer log = decoder.decode(mappedBuffer);
Matcher logMatcher = searchPattern.matcher(log);
while (logMatcher.find()) {
String match = logMatcher.group();
if (!match.isEmpty()) {
System.out.println(match);
}
}
} catch (IOException ex) {
System.err.println("thrown exception: " + ex.toString());
Throwable[] suppressed = ex.getSuppressed();
for (int i = 0; i < suppressed.length; i++) {
System.err.println("suppressed exception: "
+ suppressed[i].toString());
}
}
return;
}
このコードでは、攻撃者により正規表現インジェクションが可能である。
適合コード (ホワイトリスト化)
以下の適合コードでは、FindLogEntry()
の先頭で英数字と空白文字、シングルクオート以外を削除することで、検索パターンを無害化している。
public static void FindLogEntry(String search) {
// 検索パターンを無害化する
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 =
」という文字列での検索はできない。
適合コード (Pattern.quote()
)
以下の適合コードでは、Pattern.quote()
を使って検索文字列に含まれる悪意ある文字をエスケープ処理することで、検索パターンを無害化している。前述の適合コードとは異なり、「name =
」などの記号を含む検索文字列も使える。
public static void FindLogEntry(String search) {
// 検索文字列を無害化する
search = Pattern.quote(search);
// ユーザ入力文字列から正規表現を組み立てる
String regex = "(.*? +public\\[\\d+\\] +.*" + search + ".*)";
// ...
}
正規表現による置換で使われる文字列をエスケープ処理する際には、Matcher.quoteReplacement()
メソッドが使える。
適合コード
別の対策方法として、マッチングを行う前にセンシティブな情報を削除しておくというやり方もある。しかしこの方法では、ログファイルを読み込み直す度にセンシティブな情報の削除が必要であり、コードが複雑になり、パフォーマンスの悪化を招く。また、ログエントリの形式が変更されてもコードがそれに合わせて修正されないと、依然として情報漏えいにつながるおそれがある。
リスク評価
信頼できないデータを無害化せずに正規表現を組み立てると、情報漏えいにつながる可能性がある。
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
IDS08-J |
中 |
低 |
中 |
P4 |
L3 |
自動検出
ツール | バージョン | チェッカー | 説明 |
---|---|---|---|
The Checker Framework |
2.1.3 |
Tainting Checker | Trust and security errors (see Chapter 8) |
CodeSonar |
8.1p0
|
JAVA.IO.TAINT.REGEX |
Tainted Regular Expression (Java) |
SonarQube |
9.9
|
Regular expressions should not be vulnerable to Denial of Service attacks |
関連ガイドライン
CWE-625, Permissive Regular Expression |
参考文献
[CVE 05] |
|
[Java Tutorials] | Regular Expressions |
[Seacord 2015] |
翻訳元
これは以下のページを翻訳したものです。
IDS08-J. Sanitize untrusted data included in a regular expression (revision 123)