001package com.streamconverter.command.impl.xml; 002 003import com.streamconverter.StreamProcessingException; 004import com.streamconverter.command.ConsumerCommand; 005import com.streamconverter.security.SecureXmlConfiguration; 006import com.streamconverter.util.ClasspathResourceValidator; 007import java.io.IOException; 008import java.io.InputStream; 009import java.net.URL; 010import java.util.Objects; 011import javax.xml.XMLConstants; 012import javax.xml.transform.stream.StreamSource; 013import javax.xml.validation.Schema; 014import javax.xml.validation.SchemaFactory; 015import javax.xml.validation.Validator; 016import org.slf4j.Logger; 017import org.slf4j.LoggerFactory; 018import org.xml.sax.SAXException; 019 020/** 021 * XMLのバリデーションを行うコマンドクラス 022 * 023 * <p>XMLのバリデーションを行うコマンドクラスです。 024 * 025 * <p>このクラスは、XMLのスキーマを指定して、XMLのバリデーションを行います。 026 * 027 * <p>バリデーションエラーが発生した場合は、エラーメッセージを出力します。 028 */ 029public class ValidateCommand extends ConsumerCommand { 030 private static final Logger logger = LoggerFactory.getLogger(ValidateCommand.class); 031 private static final Logger securityLogger = 032 LoggerFactory.getLogger("com.streamConverter.security"); 033 034 private final String schemaPath; 035 private final Schema schema; 036 037 /** 038 * コンストラクタ 039 * 040 * <p>クラスパスからXMLスキーマを読み込み、バリデーションコマンドを作成します。 041 * 042 * <p>セキュリティ: ClassLoaderはクラスパス内でパス正規化を行います(例: "hoge/../fuga" → "fuga")。 043 * ただし、クラスパス境界外へのアクセスは不可能です("../etc/passwd" → リソース未発見)。 044 * 045 * @param schemaPath クラスパスリソース識別子(例: "schemas/test.xsd", "test-schema.xsd") 046 * @throws StreamProcessingException スキーマファイルの読み込みに失敗した場合 047 */ 048 public ValidateCommand(String schemaPath) { 049 Objects.requireNonNull(schemaPath, "Schema path cannot be null"); 050 if (schemaPath.trim().isEmpty()) { 051 throw new IllegalArgumentException("Schema path cannot be empty"); 052 } 053 054 // クラスパスリソース識別子として扱う(セキュリティはClasspathResourceValidatorが担保) 055 this.schemaPath = normalizeClasspathPath(schemaPath); 056 this.schema = loadSchemaFromClasspath(this.schemaPath); 057 } 058 059 /** 060 * クラスパスリソース識別子を正規化します 061 * 062 * <p>先頭のスラッシュはClassLoader互換性のため除去されます。 063 * 064 * @param inputPath 入力されたクラスパス識別子 065 * @return 正規化されたクラスパス識別子 066 */ 067 private String normalizeClasspathPath(String inputPath) { 068 String trimmed = inputPath.trim(); 069 // Remove leading slash for ClassLoader compatibility 070 if (trimmed.startsWith("/")) { 071 trimmed = trimmed.substring(1); 072 } 073 return trimmed; 074 } 075 076 /** 077 * セキュアにスキーマをロードします 078 * 079 * @param validatedPath 検証済みのスキーマパス 080 * @return ロードされたSchemaオブジェクト 081 * @throws StreamProcessingException スキーマロードに失敗した場合 082 */ 083 private Schema loadSchemaFromClasspath(String validatedPath) { 084 try { 085 // セキュアなSchemaFactoryの作成(新しいセキュリティインフラを使用) 086 SchemaFactory factory = SecureXmlConfiguration.createSecureSchemaFactory(); 087 088 // クラスパスからスキーマをロード(パストラバーサル不要・JAR対応) 089 URL schemaUrl = ClasspathResourceValidator.getResourceUrl(validatedPath); 090 091 Schema loadedSchema = factory.newSchema(schemaUrl); 092 logger.info("XML Schema loaded successfully from: {}", validatedPath); 093 securityLogger.info("Secure XML schema loading completed for: {}", validatedPath); 094 095 return loadedSchema; 096 097 } catch (SAXException | IllegalArgumentException e) { 098 logger.error("Failed to load XML schema from {}: {}", validatedPath, e.getMessage(), e); 099 securityLogger.error("Secure XML schema loading failed for: {}", validatedPath); 100 throw new StreamProcessingException( 101 String.format("XMLスキーマの読み込みに失敗しました - スキーマ: %s, エラー: %s", validatedPath, e.getMessage()), 102 e); 103 } 104 } 105 106 /** 107 * XMLのバリデーションを行うコマンドを実行します。 108 * 109 * <p>XMLのスキーマを指定して、XMLのバリデーションを行います。 110 * 111 * <p>バリデーションエラーが発生した場合は、エラーメッセージを出力します。 112 * 113 * @param inputStream 入力ストリーム 114 * @throws IOException 入出力エラーが発生した場合 115 * @throws StreamProcessingException XMLバリデーションエラーが発生した場合 116 */ 117 @Override 118 public void consume(InputStream inputStream) throws IOException { 119 Objects.requireNonNull(inputStream, "Input stream cannot be null"); 120 121 try { 122 // セキュアなValidatorの作成 123 Validator validator = schema.newValidator(); 124 configureSecureValidator(validator); 125 126 // XMLバリデーションの実行 127 validator.validate(new StreamSource(inputStream)); 128 129 logger.info("XML validation completed successfully using schema: {}", schemaPath); 130 131 } catch (SAXException e) { 132 // バリデーションエラーの詳細ログ出力 133 logger.error("XMLバリデーションエラーが発生しました: {}", e.getMessage(), e); 134 135 // バリデーションエラーをカスタム例外でラップして伝播 136 throw new StreamProcessingException( 137 String.format("XMLバリデーションに失敗しました - スキーマ: %s, エラー: %s", schemaPath, e.getMessage()), e); 138 } 139 } 140 141 /** 142 * Validatorにセキュリティ設定を適用します 143 * 144 * @param validator 設定対象のValidator 145 */ 146 private void configureSecureValidator(Validator validator) { 147 try { 148 // XXE攻撃防止設定 149 validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 150 151 // 外部リソースアクセスを無効化 152 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 153 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 154 155 logger.debug("Secure XML processing features configured for Validator"); 156 157 } catch (Exception e) { 158 logger.warn("Could not configure all security features for Validator: {}", e.getMessage()); 159 // 警告レベルで記録し、処理は継続 160 } 161 } 162}