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}