Java/Kotlinリポジトリで、@Patternによってファイル名を制限しているにもかかわらず、CodeQLのjava/path-injectionが警告を出し続けるケースがあります。
CodeQL 2.26.1では、@javax.validation.constraints.Patternによる正規表現チェックをパスインジェクションのサニタイザーとして認識できるようになり、この種の誤検知が減少します。ただし、@Patternを付ければ無条件に安全になるわけではありません。正規表現の内容、アノテーションの配置、実行時バリデーションの有効化、パスの組み立て方によっては、警告が残るか、警告が消えても脆弱性が残る可能性があります。(The GitHub Blog)
この記事では、CodeQL 2.26.1で何が変わったのか、誤検知を減らしやすい実装方法、警告が残る場合の確認ポイントまで具体的に解説します。
@Patternで検証したJava入力をCodeQL 2.26.1はパスインジェクションとして検出する?
結論として、パス区切り文字やディレクトリ移動を許可しない正規表現で入力を制約しており、その検証をCodeQLが追跡できる場合は、従来よりjava/path-injectionの警告が出にくくなります。
一方、次のような実装は引き続き警告される可能性があります。
| 実装 | CodeQL 2.26.1での見込み | セキュリティ上の判断 |
|---|---|---|
@Pattern(regexp = "[A-Za-z0-9_-]{1,64}")でファイルIDを制限 | サニタイザーとして認識されやすい | 単一IDとして使うなら安全性を高めやすい |
@Pattern(regexp = ".*\\.pdf")で拡張子だけを確認 | 警告が残る可能性が高い | ../../secret.pdfを許可するため不十分 |
/や\を許可する正規表現 | 警告が残る可能性がある | 任意の相対パスを組み立てられる恐れがある |
独自の@SafeFileNameアノテーション | CodeQLがモデル化できない場合がある | 実装が安全でも解析上は未検証になる可能性がある |
@Patternはあるが実行時検証が動いていない | 警告が消える可能性はある | 実際のアプリケーションは安全とは限らない |
| 検証済みの値以外にも外部入力を連結 | 警告が残る | パス全体のデータフローを確認する必要がある |
CodeQLのクエリヘルプでも、単一のファイル名を受け取る場合は、/、\、..などを拒否するか、許可リスト方式の正規表現を使うことが推奨されています。(CodeQL)
CodeQL 2.26.1で変わった仕組み
@Patternを正規表現チェックとして追跡できるようになった
CodeQLは、外部入力からファイル操作までのデータフローを追跡します。
概念的には、次の流れです。
HTTPリクエストなどの外部入力
↓
DTO・メソッド引数
↓
ファイルパスの生成
↓
File、Path、FileInputStreamなどのファイル操作
途中で信頼できる入力検証が行われた場合、その処理を「サニタイザー」として扱い、危険なデータフローを遮断します。
CodeQL 2.26.1のJavaライブラリでは、@Patternを正規表現チェックの一種としてモデル化しています。具体的には、次の場所に付けられたアノテーションから値を追跡します。
- フィールドへの直接アクセス
- アノテーション付きフィールドのGetter
- メソッドやコンストラクターの引数
- アノテーション付きメソッドの戻り値
一方、型そのものに付けられたアノテーションは、2.26.1の公開ソース上では未対応のケースとして残されています。そのため、値オブジェクトの型アノテーションや複雑な独自制約では、CodeQLが検証済みと判断できない場合があります。(GitHub)
javaxだけでなくjakartaも実装上は対象になる
公式の変更履歴では、@javax.validation.constraints.Patternという名称で説明されています。
ただし、CodeQL 2.26.1の公開ソースでは、パッケージ名の判定にjavaxOrJakarta()が使われています。そのため、実装上は次の両方がモデルの対象になります。
javax.validation.constraints.Pattern
jakarta.validation.constraints.Pattern
Spring Boot 3以降などでjakarta.validationを使用しているリポジトリでも、同じ解析改善の対象になると考えられます。ただし、実際の結果は使用中のCodeQL query packで確認することが重要です。(GitHub)
正規表現の内容まで安全性判定に使われる
重要なのは、CodeQLが単に「@Patternが付いているか」だけを確認しているわけではない点です。
2.26.1のパスサニタイザー実装では、正規表現がディレクトリ操作につながる文字を除外しているかを確認します。公開ソースでは、主に次の文字を許可しないパターンを安全側のチェックとして扱うロジックが実装されています。
.
/
\
そのため、次のような正規表現はサニタイザーとして認識されやすくなります。
@Pattern(regexp = "[A-Za-z0-9_-]{1,64}")
反対に、次のような正規表現はパスインジェクション対策になりません。
@Pattern(regexp = ".*")
@Pattern(regexp = ".*\\.pdf")
@Pattern(regexp = "[A-Za-z0-9_./\\\\-]+")
また、意味としては安全な正規表現でも、複雑な先読み、独自の文字クラス、複数段階の条件分岐などを使うと、CodeQLが意図を解析できず警告が残る場合があります。静的解析で認識されやすくするには、入力仕様を単純化し、明確な許可リストを使うことが効果的です。(GitHub)
誤検知を減らしやすいJavaの実装例
パスそのものを利用者に入力させるのではなく、ファイルを識別するIDだけを受け取る設計が安全です。
以下はjakarta.validationを使った例です。javax.validationを利用している環境では、import部分を読み替えてください。
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
public final class DownloadRequest {
@NotBlank
@Pattern(regexp = "[A-Za-z0-9_-]{1,64}")
private String fileKey;
public String getFileKey() {
return fileKey;
}
public void setFileKey(String fileKey) {
this.fileKey = fileKey;
}
}
パスを生成する処理では、固定したルートディレクトリの下に解決し、正規化後もその配下に収まっていることを確認します。
import java.nio.file.Path;
public final class ReportPathResolver {
private static final Path REPORT_ROOT =
Path.of("/srv/app/reports")
.toAbsolutePath()
.normalize();
public Path resolve(String fileKey) {
Path candidate = REPORT_ROOT
.resolve(fileKey + ".pdf")
.normalize();
if (!candidate.startsWith(REPORT_ROOT)) {
throw new IllegalArgumentException("Invalid file path");
}
return candidate;
}
}
Spring MVCでリクエストDTOを受け取る場合は、実際にBean Validationが実行されるようにします。
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ReportController {
private final ReportPathResolver pathResolver;
public ReportController(ReportPathResolver pathResolver) {
this.pathResolver = pathResolver;
}
@GetMapping("/reports/download")
public String download(
@Valid @ModelAttribute DownloadRequest request) {
Path target = pathResolver.resolve(request.getFileKey());
// 実際にはResourceとして返すなどの処理を行う
return target.toString();
}
}
この実装には、次の利点があります。
- 利用者はファイルパスではなくファイルIDだけを指定する
- 正規表現が
.、/、\を許可しない .pdfという拡張子はサーバー側で追加するPath.normalize()後に保存先ディレクトリ内であることを確認する- 正規表現チェックとパス包含チェックを二重に適用する
@Patternではnullが有効な値として扱われます。そのため、必須入力では@NotNullや@NotBlankを併用する必要があります。(Oracle Docs)
また、Spring MVCでは、リクエストDTOに付けたBean Validation制約を実行するために、通常は@Validまたは@Validatedを利用します。CodeQLがアノテーションをサニタイザーとして認識していても、アプリケーションの実行時に検証が呼び出されていなければ安全性は確保できません。(Home)
拡張子だけを確認する@Patternが危険な理由
次のコードは、一見するとPDFファイルだけを許可しているように見えます。
@Pattern(regexp = ".*\\.pdf")
private String fileName;
しかし、次の入力も正規表現に一致します。
../../confidential.pdf
/tmp/report.pdf
..\..\config\secret.pdf
つまり、拡張子を限定しても、ディレクトリ移動や絶対パスの指定を防止できません。
ファイル名を利用者に指定させる必要がある場合でも、次のように考えると安全です。
| 入力させるもの | 推奨方法 |
|---|---|
| 帳票や添付ファイル | ファイルIDを受け取り、DBやサーバー側テーブルで実ファイルに変換する |
| 固定種類のテンプレート | invoice、receiptなどの列挙値に限定する |
| 単一の保存名 | 英数字、ハイフン、アンダースコアだけに限定する |
| 階層を含む相対パス | 正規表現だけに頼らず、正規化後の包含チェックを必ず行う |
| 任意の絶対パス | 原則として外部入力では受け付けない |
日本語の表示ファイル名が必要な場合も、保存用のキーと表示名を分ける設計が有効です。
保存キー: 8f93c128-2026-report
表示名: 2026年度事業報告書.pdf
ファイルシステム上のパスには保存キーだけを使い、ダウンロード時のHTTPヘッダーで表示名を設定すれば、パス入力そのものを利用者に制御させずに済みます。
Kotlinではアノテーションの付与先を明示する
Kotlinのプロパティは、Javaバイトコード上ではフィールド、Getter、コンストラクター引数など複数の要素に展開されます。
そのため、Bean ValidationとCodeQLの両方に意図を明確に伝えるには、use-site targetを明示する方法が確実です。
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
data class DownloadRequest(
@field:NotBlank
@field:Pattern(regexp = "[A-Za-z0-9_-]{1,64}")
val fileKey: String
)
Getterに制約を配置したい場合は、次のように記述できます。
data class DownloadRequest(
@get:Pattern(regexp = "[A-Za-z0-9_-]{1,64}")
val fileKey: String
)
Kotlinでは、use-site targetを省略した場合のアノテーション配置が、利用する言語バージョンやアノテーションの@Targetによって変わる可能性があります。@field:、@get:、@param:を明示すれば、実行時検証と静的解析の双方で挙動を確認しやすくなります。(Kotlin)
CodeQL 2.26.1でも警告が残る主な原因
実際に使われているCodeQLが古い
ローカルのCodeQL CLIを使っている場合は、次のコマンドでバージョンを確認できます。
codeql version --format=terse
codeql versionは、現在使用しているCodeQLツールチェーンのバージョンを表示する公式コマンドです。(GitHub Docs)
ただし、CLIだけを更新し、古いquery packを組み合わせている環境では、期待する解析変更が反映されない可能性があります。GitHubは、互換性のあるCLI、クエリ、ライブラリ、コンパイル済みクエリをまとめたCodeQL bundleの利用を推奨しています。(GitHub Docs)
正規表現が緩すぎる
次の文字を許可していないか確認します。
.
/
\
特に、.*や.+を含む正規表現は注意が必要です。
拡張子を含む実際のファイル名を入力させるより、拡張子を除いたIDを入力させ、サーバー側で拡張子を付与する方が、セキュリティとCodeQLの解析精度の両方で有利です。
正規表現が複雑でCodeQLに認識されていない
CodeQLのサニタイザー判定は、任意の正規表現を完全に証明するものではありません。
次のような表現では、意味上は安全でも警告が残る可能性があります。
- 複雑な先読みや後読み
- 複数の否定条件を組み合わせた正規表現
- 独自フラグを使った正規表現
- 独自アノテーション内に合成した
@Pattern - 型使用位置だけに付けたアノテーション
警告が残った場合は、無理にCodeQLへ合わせて可読性を下げるのではなく、まずデータフローとパス包含チェックを確認します。安全性を確認できた後で、必要に応じてカスタムモデルや適切なアラート処理を検討します。
検証後に別の外部入力を追加している
次の実装では、fileKeyを検証していても、directoryが外部入力なら安全ではありません。
Path target = root
.resolve(directory)
.resolve(fileKey + ".pdf");
CodeQLはパス全体へのデータフローを確認するため、検証済みの変数が一つあっても、別の未検証入力が流入すれば警告を出します。
対象が別のCodeQLクエリである
表示されているクエリIDが本当にjava/path-injectionか確認してください。
パスに関係する警告には、ほかにも次のようなものがあります。
java/partial-path-traversal
java/partial-path-traversal-from-remote
java/zipslip
今回の@Pattern認識改善は、すべてのパス関連クエリを無条件に抑制する変更ではありません。
パス包含チェックではPath.startsWithを使う
正規化後のパスを確認するときは、文字列のstartsWith()ではなく、Path.startsWith()を使う方法が安全です。
次の文字列比較は、意図しない兄弟ディレクトリまで許可する可能性があります。
if (!candidate.toString().startsWith(root.toString())) {
throw new IllegalArgumentException();
}
例えば、ルートが次の場合を考えます。
/srv/app/report
文字列比較では、次のパスも同じ文字列から始まります。
/srv/app/report-backup/secret.pdf
Path.startsWith()はパス要素単位で判定するため、このような部分一致を避けられます。
Path normalizedRoot = root.toAbsolutePath().normalize();
Path normalizedCandidate = candidate.toAbsolutePath().normalize();
if (!normalizedCandidate.startsWith(normalizedRoot)) {
throw new IllegalArgumentException("Invalid file path");
}
CodeQLの関連クエリでも、正規化したPathに対してPath.startsWith()を使う実装が安全な例として示されています。(CodeQL)
アップデート後に行う確認手順
使用中の解析環境を確認する
GitHub.comの標準的なcode scanningでは、新しいCodeQLバージョンが自動展開されます。一方、古いGitHub Enterprise Serverや外部のCodeQL CLIを使っている場合は、手動更新が必要になることがあります。(The GitHub Blog)
確認する項目は次のとおりです。
CodeQL CLIのバージョン
使用中のCodeQL bundle
codeql/java-queriesのバージョン
GitHub Enterprise Serverのバージョン
カスタムquery packの有無
同じコミットを同じ条件で再解析する
変更前後の結果を比較するときは、次の条件をそろえます。
同じソースコード
同じビルド方法
同じクエリスイート
同じ脅威モデル設定
同じカスタムクエリ
単にリポジトリ全体のアラート件数を比較するのではなく、クエリIDをjava/path-injectionに絞って確認してください。
消えたアラートもコードレビューする
CodeQL 2.26.1でアラートが消えた場合でも、次の項目を確認します。
| 確認項目 | 判断基準 |
|---|---|
| 正規表現 | /、\、.、..などを許可していないか |
| バリデーション | 実際のリクエスト処理で実行されているか |
| パス生成 | 固定ルートからresolve()しているか |
| 正規化 | ファイル操作前にnormalize()しているか |
| 包含確認 | Path.startsWith()でルート配下を確認しているか |
| シンボリックリンク | 脅威モデルに応じて実体パスの確認が必要でないか |
| 権限 | アプリケーションのOSユーザーに過剰なファイル権限がないか |
java/path-injectionは公式ドキュメント上でPrecisionがHigh、Security severityが7.5に設定されています。更新後も残るアラートを、単なる誤検知と決めつけて一括で無視するのは避けるべきです。(CodeQL)
悪意ある入力のテストを追加する
先ほどのファイルID方式では、次の結果になることを自動テストで確認します。
| 入力 | 期待結果 |
|---|---|
report_2026 | 許可 |
report.pdf | 拒否 |
../report | 拒否 |
..\report | 拒否 |
reports/2026 | 拒否 |
/etc/passwd | 拒否 |
C:\Windows\win.ini | 拒否 |
%2e%2e%2freport | 実際のHTTPデコード後の値で拒否 |
URLエンコードされた入力は、Webフレームワークがどの段階でデコードするかによって検証対象が変わります。正規表現単体のユニットテストだけでなく、HTTPリクエストからファイル操作直前までを通す結合テストも用意すると確実です。
公開日とバージョン履歴の注意点
GitHub ChangelogでCodeQL 2.26.1が紹介されたのは2026年7月29日ですが、CodeQL CLIの変更履歴では2.26.1が2026年7月15日付として掲載されています。(The GitHub Blog)
また、CodeQL 2.24.2の変更履歴にも、@javax.validation.constraints.Patternをjava/path-injectionなどのサニタイザーとして認識するという同趣旨の説明があります。(CodeQL)
そのため、2.26.1を「@Pattern対応が初めて実装されたバージョン」と断定するのは避けるべきです。実務上は、次のように整理すると正確です。
CodeQL 2.26.1の公式変更として、
java/path-injectionが@Patternによる検証を
サニタイザーとして扱うことが改めて明示された
解析結果を判断するときは、告知の日付だけでなく、実際に使用されているCodeQL bundleとquery packのバージョンを確認してください。
CodeQLの警告削減と実際の安全性を分けて考える
CodeQL 2.26.1では、@Patternを使った正当な入力検証がjava/path-injectionの解析に反映され、従来の誤検知パターンが減少します。
ただし、対応後も次の原則は変わりません。
- 外部から任意のパスを受け取らない
- ファイル名よりファイルIDを受け取る
- 単純な許可リスト方式の正規表現を使う
- Bean Validationが実行時に動いていることを確認する
- 固定ルートからパスを解決する
- 正規化後に
Path.startsWith()で包含関係を確認する - 警告が消えても悪意ある入力のテストを残す
まず、使用中のCodeQLバージョンとquery packを確認してください。そのうえで、@Patternの正規表現がパス区切り文字を許可していないか、実行時検証が有効か、正規化後の包含チェックがあるかを順番に確認することが、誤検知削減と脆弱性防止を両立する最も確実な進め方です。

コメント