001package com.streamconverter.security;
002
003import java.util.regex.Pattern;
004import org.slf4j.Logger;
005import org.slf4j.LoggerFactory;
006
007/**
008 * XPathインジェクション攻撃を防ぐためのバリデータークラス
009 *
010 * <p>このクラスは、XPath式の安全性を検証し、 悪意のあるXPath式を検出・ブロックします。
011 *
012 * <p>主な機能:
013 *
014 * <ul>
015 *   <li>XPathインジェクション攻撃パターンの検出
016 *   <li>危険な関数の使用制限
017 *   <li>特殊文字のエスケープ処理
018 *   <li>XPath式の構造的検証
019 *   <li>セキュリティ設定による動的制御
020 * </ul>
021 *
022 * @since 1.0.0
023 */
024public class SecureXPathValidator {
025
026  private static final Logger securityLogger =
027      LoggerFactory.getLogger("com.streamConverter.security");
028
029  // XPathインジェクション攻撃パターン(エスケープクォートも含む)
030  private static final Pattern XPATH_INJECTION_PATTERN =
031      Pattern.compile(
032          ".*(;|'\\s*or\\s*'|\"\\s*or\\s*\"|\\\\['\"\\\\]|\\b(union|drop|insert|delete)\\b).*",
033          Pattern.CASE_INSENSITIVE);
034
035  // 危険な関数パターン
036  private static final Pattern DANGEROUS_FUNCTIONS_PATTERN =
037      Pattern.compile(
038          ".*\\b(document|unparsed-text|collection|doc|fn:doc|fn:collection|sql:execute)\\s*\\(.*",
039          Pattern.CASE_INSENSITIVE);
040
041  // 外部参照パターン
042  private static final Pattern EXTERNAL_REFERENCE_PATTERN =
043      Pattern.compile(".*(http://|https://|file://|ftp://|\\.\\./).*", Pattern.CASE_INSENSITIVE);
044
045  // SQLインジェクション類似パターン
046  private static final Pattern SQL_LIKE_INJECTION_PATTERN =
047      Pattern.compile(
048          ".*(union|select|insert|update|delete|drop|create|alter|exec|execute)\\s+.*",
049          Pattern.CASE_INSENSITIVE);
050
051  private SecureXPathValidator() {
052    // ユーティリティクラスのため、インスタンス化を禁止
053  }
054
055  /**
056   * XPath式の安全性を検証します
057   *
058   * @param xpath 検証対象のXPath式
059   * @throws SecurityException XPath式が安全でない場合
060   * @throws IllegalArgumentException xpath引数がnullまたは空の場合
061   */
062  public static void validateXPath(String xpath) {
063    if (xpath == null || xpath.trim().isEmpty()) {
064      throw new IllegalArgumentException("TreePath expression cannot be null or empty");
065    }
066
067    String trimmedXpath = xpath.trim();
068
069    // 基本的なインジェクションパターンチェック
070    validateBasicInjectionPatterns(trimmedXpath);
071
072    // 危険な関数の使用チェック
073    validateDangerousFunctions(trimmedXpath);
074
075    // 外部参照の検出
076    validateExternalReferences(trimmedXpath);
077
078    // SQLインジェクション類似パターンの検出
079    validateSqlLikeInjection(trimmedXpath);
080
081    // 厳格モードでの追加検証
082    validateStrictMode(trimmedXpath);
083
084    securityLogger.debug("TreePath validation passed: {}", sanitizeForLogging(trimmedXpath));
085  }
086
087  /**
088   * XPath式をサニタイズします(エスケープ処理)
089   *
090   * @param xpath サニタイズ対象のXPath式
091   * @return サニタイズされたXPath式
092   */
093  public static String sanitizeXPath(String xpath) {
094    if (xpath == null) {
095      return null;
096    }
097
098    String sanitized = xpath;
099
100    // 危険な文字を除去(エスケープではなく除去)
101    sanitized = sanitized.replaceAll("[';\"]", "");
102    sanitized = sanitized.replace(";", "");
103
104    // 改行コードの除去
105    sanitized = sanitized.replace("\\r", "");
106    sanitized = sanitized.replace("\\n", " ");
107
108    // 連続する空白の正規化
109    sanitized = sanitized.replaceAll("\\s+", " ");
110
111    securityLogger.debug(
112        "TreePath sanitized: {} -> {}", sanitizeForLogging(xpath), sanitizeForLogging(sanitized));
113
114    return sanitized;
115  }
116
117  /**
118   * XPath式が安全かどうかを判定します(例外を投げない版)
119   *
120   * @param xpath 検証対象のXPath式
121   * @return 安全な場合true、そうでない場合false
122   */
123  public static boolean isXPathSafe(String xpath) {
124    try {
125      validateXPath(xpath);
126      return true;
127    } catch (SecurityException | IllegalArgumentException e) {
128      securityLogger.warn("Unsafe TreePath detected: {}", sanitizeForLogging(xpath));
129      return false;
130    }
131  }
132
133  /**
134   * 許可されたXPath関数のリストを取得します
135   *
136   * @return 許可された関数の配列
137   */
138  public static String[] getAllowedXPathFunctions() {
139    return new String[] {
140      "text()",
141      "node()",
142      "position()",
143      "last()",
144      "count()",
145      "name()",
146      "local-name()",
147      "namespace-uri()",
148      "boolean()",
149      "number()",
150      "string()",
151      "string-length()",
152      "concat()",
153      "substring()",
154      "substring-before()",
155      "substring-after()",
156      "normalize-space()",
157      "translate()",
158      "contains()",
159      "starts-with()",
160      "sum()",
161      "floor()",
162      "ceiling()",
163      "round()"
164    };
165  }
166
167  // ===========================================
168  // Private Helper Methods
169  // ===========================================
170
171  private static void validateBasicInjectionPatterns(String xpath) {
172    if (XPATH_INJECTION_PATTERN.matcher(xpath).matches()) {
173      securityLogger.warn(
174          "Basic TreePath injection pattern detected: {}", sanitizeForLogging(xpath));
175      throw new SecurityException(
176          "Potentially malicious TreePath expression detected: basic injection pattern");
177    }
178  }
179
180  private static void validateDangerousFunctions(String xpath) {
181    if (DANGEROUS_FUNCTIONS_PATTERN.matcher(xpath).matches()) {
182      securityLogger.warn("Dangerous TreePath function detected: {}", sanitizeForLogging(xpath));
183      throw new SecurityException(
184          "Potentially malicious TreePath expression detected: dangerous function usage");
185    }
186  }
187
188  private static void validateExternalReferences(String xpath) {
189    if (EXTERNAL_REFERENCE_PATTERN.matcher(xpath).matches()) {
190      securityLogger.warn("External reference in TreePath detected: {}", sanitizeForLogging(xpath));
191      throw new SecurityException(
192          "Potentially malicious TreePath expression detected: external reference");
193    }
194  }
195
196  private static void validateSqlLikeInjection(String xpath) {
197    if (SQL_LIKE_INJECTION_PATTERN.matcher(xpath).matches()) {
198      securityLogger.warn(
199          "SQL-like injection pattern in TreePath detected: {}", sanitizeForLogging(xpath));
200      throw new SecurityException(
201          "Potentially malicious TreePath expression detected: SQL-like injection pattern");
202    }
203  }
204
205  private static void validateStrictMode(String xpath) {
206    // 厳格モードでの追加検証
207
208    // 長すぎるXPath式を拒否
209    if (xpath.length() > 1000) {
210      securityLogger.warn(
211          "TreePath expression too long in strict mode: {} characters", xpath.length());
212      throw new SecurityException("TreePath expression exceeds maximum length in strict mode");
213    }
214
215    // 深いネストを拒否
216    long nestingLevel = xpath.chars().filter(ch -> ch == '[').count();
217    if (nestingLevel > 10) {
218      securityLogger.warn(
219          "TreePath expression has too deep nesting in strict mode: {} levels", nestingLevel);
220      throw new SecurityException("TreePath expression has excessive nesting in strict mode");
221    }
222
223    // 複雑な演算子の組み合わせを制限
224    if (xpath.contains("and") && xpath.contains("or") && xpath.contains("not")) {
225      securityLogger.warn(
226          "Complex operator combination in TreePath in strict mode: {}", sanitizeForLogging(xpath));
227      throw new SecurityException("Complex operator combinations not allowed in strict mode");
228    }
229
230    // ワイルドカードの過度な使用を制限
231    long wildcardCount = xpath.chars().filter(ch -> ch == '*').count();
232    if (wildcardCount > 5) {
233      securityLogger.warn(
234          "Excessive wildcard usage in TreePath in strict mode: {} wildcards", wildcardCount);
235      throw new SecurityException("Excessive wildcard usage not allowed in strict mode");
236    }
237  }
238
239  private static String sanitizeForLogging(String xpath) {
240    if (xpath == null) {
241      return "null";
242    }
243
244    // ログ出力用のサニタイズ(機密情報や長すぎる文字列の対策)
245    String sanitized = xpath;
246
247    // 長すぎる場合は省略
248    if (sanitized.length() > 100) {
249      sanitized = sanitized.substring(0, 97) + "...";
250    }
251
252    // 制御文字を除去
253    sanitized = sanitized.replaceAll("[\\p{Cntrl}\\p{Cc}\\p{Cf}\\p{Co}\\p{Cn}]", "");
254
255    return sanitized;
256  }
257}