001package com.streamconverter.command.impl.xml; 002 003import com.streamconverter.StreamProcessingException; 004import com.streamconverter.command.ConsumerCommand; 005import com.streamconverter.config.SecurityConfigurationManager; 006import com.streamconverter.security.SecureXmlConfiguration; 007import java.io.IOException; 008import java.io.InputStream; 009import java.net.URL; 010import java.nio.file.Path; 011import java.nio.file.Paths; 012import java.util.Objects; 013import javax.xml.XMLConstants; 014import javax.xml.transform.stream.StreamSource; 015import javax.xml.validation.Schema; 016import javax.xml.validation.SchemaFactory; 017import javax.xml.validation.Validator; 018import org.slf4j.Logger; 019import org.slf4j.LoggerFactory; 020import org.xml.sax.SAXException; 021 022/** 023 * XMLのバリデーションを行うコマンドクラス 024 * 025 * <p>XMLのバリデーションを行うコマンドクラスです。 026 * 027 * <p>このクラスは、XMLのスキーマを指定して、XMLのバリデーションを行います。 028 * 029 * <p>バリデーションエラーが発生した場合は、エラーメッセージを出力します。 030 */ 031public class ValidateCommand extends ConsumerCommand { 032 private static final Logger logger = LoggerFactory.getLogger(ValidateCommand.class); 033 private static final Logger securityLogger = 034 LoggerFactory.getLogger("com.streamConverter.security"); 035 036 private static final SecurityConfigurationManager securityConfig = 037 SecurityConfigurationManager.getInstance(); 038 039 /** スキーマファイルのベースディレクトリ(セキュリティのため固定) */ 040 private static final Path SCHEMA_BASE_PATH = Paths.get("schemas"); 041 042 private final String schemaPath; 043 private final Schema schema; 044 045 /** 046 * コンストラクタ 047 * 048 * <p>XMLのスキーマを指定して、XMLのバリデーションを行います。 セキュリティのため、パストラバーサル攻撃を防止します。 049 * 050 * @param schemaPath XMLのスキーマファイルパス(schemas/ディレクトリからの相対パス) 051 * @throws StreamProcessingException スキーマファイルの読み込みに失敗した場合 052 * @throws SecurityException 不正なパスが指定された場合 053 */ 054 public ValidateCommand(String schemaPath) { 055 Objects.requireNonNull(schemaPath, "Schema path cannot be null"); 056 if (schemaPath.trim().isEmpty()) { 057 throw new IllegalArgumentException("Schema path cannot be empty"); 058 } 059 060 this.schemaPath = validateAndNormalizeSchemaPath(schemaPath); 061 this.schema = loadSchemaSecurely(this.schemaPath); 062 } 063 064 /** 065 * スキーマパスを検証し、正規化します(パストラバーサル攻撃防止) 066 * 067 * @param inputPath 入力されたスキーマパス 068 * @return 安全な正規化されたパス 069 * @throws SecurityException 不正なパスが検出された場合 070 */ 071 private String validateAndNormalizeSchemaPath(String inputPath) { 072 String trimmedPath = inputPath.trim(); 073 074 // セキュリティ設定でパストラバーサル防止が無効な場合は基本検証のみ 075 if (!securityConfig.isPathTraversalPreventionEnabled()) { 076 logger.debug("Path traversal prevention is disabled"); 077 return trimmedPath; 078 } 079 080 // テスト環境での絶対パスを許可(テストリソースディレクトリのみ) 081 if (trimmedPath.startsWith("/") || trimmedPath.contains(":")) { 082 // パス区切り文字を正規化して検証(Windows/Unix対応) 083 String normalizedPath = trimmedPath.replace("\\", "/"); 084 // テストリソースパスの場合は許可 085 if (normalizedPath.contains("src/test/resources") 086 || normalizedPath.contains("build/resources/test") 087 || normalizedPath.contains("junit")) { 088 logger.debug("Test resource path allowed: {}", trimmedPath); 089 return trimmedPath; 090 } 091 securityLogger.warn("Potentially dangerous absolute path detected: {}", trimmedPath); 092 throw new SecurityException( 093 "Schema path contains potentially dangerous patterns: " + trimmedPath); 094 } 095 096 // 親ディレクトリ参照の検証 097 if (!securityConfig.isParentReferencesAllowed()) { 098 if (trimmedPath.contains("..") || trimmedPath.contains("./") || trimmedPath.contains(".\\")) { 099 securityLogger.warn("Path traversal attempt detected: {}", trimmedPath); 100 throw new SecurityException( 101 "Schema path contains potentially dangerous patterns: " + trimmedPath); 102 } 103 } 104 105 // ワークスペース制限が有効な場合の追加検証 106 if (securityConfig.isFileAccessRestrictedToWorkspace()) { 107 // ベースパスからの相対パスとして解決 108 Path resolvedPath = SCHEMA_BASE_PATH.resolve(trimmedPath).normalize(); 109 110 // ベースディレクトリ外へのアクセスを防止 111 if (!resolvedPath.startsWith(SCHEMA_BASE_PATH)) { 112 securityLogger.warn("Workspace access violation detected: {}", trimmedPath); 113 throw new SecurityException( 114 "Schema path attempts to access outside base directory: " + trimmedPath); 115 } 116 117 return resolvedPath.toString(); 118 } 119 120 return trimmedPath; 121 } 122 123 /** 124 * セキュアにスキーマをロードします 125 * 126 * @param validatedPath 検証済みのスキーマパス 127 * @return ロードされたSchemaオブジェクト 128 * @throws StreamProcessingException スキーマロードに失敗した場合 129 */ 130 private Schema loadSchemaSecurely(String validatedPath) { 131 try { 132 // セキュアなSchemaFactoryの作成(新しいセキュリティインフラを使用) 133 SchemaFactory factory = SecureXmlConfiguration.createSecureSchemaFactory(); 134 135 // ファイルからスキーマをロード 136 Path schemaFile = Paths.get(validatedPath); 137 URL schemaUrl = schemaFile.toUri().toURL(); 138 139 Schema loadedSchema = factory.newSchema(schemaUrl); 140 logger.info("XML Schema loaded successfully from: {}", validatedPath); 141 securityLogger.info("Secure XML schema loading completed for: {}", validatedPath); 142 143 return loadedSchema; 144 145 } catch (SAXException | IOException e) { 146 logger.error("Failed to load XML schema from {}: {}", validatedPath, e.getMessage(), e); 147 securityLogger.error("Secure XML schema loading failed for: {}", validatedPath); 148 throw new StreamProcessingException( 149 String.format("XMLスキーマの読み込みに失敗しました - スキーマ: %s, エラー: %s", validatedPath, e.getMessage()), 150 e); 151 } 152 } 153 154 /** 155 * XMLのバリデーションを行うコマンドを実行します。 156 * 157 * <p>XMLのスキーマを指定して、XMLのバリデーションを行います。 158 * 159 * <p>バリデーションエラーが発生した場合は、エラーメッセージを出力します。 160 * 161 * @param inputStream 入力ストリーム 162 * @throws IOException 入出力エラーが発生した場合 163 * @throws StreamProcessingException XMLバリデーションエラーが発生した場合 164 */ 165 @Override 166 public void consume(InputStream inputStream) throws IOException { 167 Objects.requireNonNull(inputStream, "Input stream cannot be null"); 168 169 try { 170 // セキュアなValidatorの作成 171 Validator validator = schema.newValidator(); 172 configureSecureValidator(validator); 173 174 // XMLバリデーションの実行 175 validator.validate(new StreamSource(inputStream)); 176 177 logger.info("XML validation completed successfully using schema: {}", schemaPath); 178 179 } catch (SAXException e) { 180 // バリデーションエラーの詳細ログ出力 181 logger.error("XMLバリデーションエラーが発生しました: {}", e.getMessage(), e); 182 183 // バリデーションエラーをカスタム例外でラップして伝播 184 throw new StreamProcessingException( 185 String.format("XMLバリデーションに失敗しました - スキーマ: %s, エラー: %s", schemaPath, e.getMessage()), e); 186 } 187 } 188 189 /** 190 * Validatorにセキュリティ設定を適用します 191 * 192 * @param validator 設定対象のValidator 193 */ 194 private void configureSecureValidator(Validator validator) { 195 try { 196 // XXE攻撃防止設定 197 validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 198 199 // 外部リソースアクセスを無効化 200 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 201 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 202 203 logger.debug("Secure XML processing features configured for Validator"); 204 205 } catch (Exception e) { 206 logger.warn("Could not configure all security features for Validator: {}", e.getMessage()); 207 // 警告レベルで記録し、処理は継続 208 } 209 } 210}