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 private ValidateCommand(String schemaPath, Schema schema) { 049 this.schemaPath = schemaPath; 050 this.schema = schema; 051 } 052 053 /** 054 * Factory method for creating a ValidateCommand. 055 * 056 * @param schemaPath クラスパスリソース識別子(例: "schemas/test.xsd", "test-schema.xsd") 057 * @return a ValidateCommand instance 058 * @throws NullPointerException スキーマパスがnullの場合 059 * @throws IllegalArgumentException スキーマパスが空の場合 060 * @throws StreamProcessingException スキーマファイルの読み込みに失敗した場合 061 */ 062 public static ValidateCommand create(String schemaPath) { 063 Objects.requireNonNull(schemaPath, "Schema path cannot be null"); 064 if (schemaPath.trim().isEmpty()) { 065 throw new IllegalArgumentException("Schema path cannot be empty"); 066 } 067 String normalizedPath = normalizeClasspathPath(schemaPath); 068 Schema schema = loadSchemaFromClasspath(normalizedPath); 069 return new ValidateCommand(normalizedPath, schema); 070 } 071 072 /** 073 * クラスパスリソース識別子を正規化します 074 * 075 * <p>先頭のスラッシュはClassLoader互換性のため除去されます。 076 * 077 * @param inputPath 入力されたクラスパス識別子 078 * @return 正規化されたクラスパス識別子 079 */ 080 private static String normalizeClasspathPath(String inputPath) { 081 String trimmed = inputPath.trim(); 082 // Remove leading slash for ClassLoader compatibility 083 if (trimmed.startsWith("/")) { 084 trimmed = trimmed.substring(1); 085 } 086 return trimmed; 087 } 088 089 /** 090 * セキュアにスキーマをロードします 091 * 092 * @param validatedPath 検証済みのスキーマパス 093 * @return ロードされたSchemaオブジェクト 094 * @throws StreamProcessingException スキーマロードに失敗した場合 095 */ 096 private static Schema loadSchemaFromClasspath(String validatedPath) { 097 try { 098 // セキュアなSchemaFactoryの作成(新しいセキュリティインフラを使用) 099 SchemaFactory factory = SecureXmlConfiguration.createSecureSchemaFactory(); 100 101 // クラスパスからスキーマをロード(パストラバーサル不要・JAR対応) 102 URL schemaUrl = ClasspathResourceValidator.getResourceUrl(validatedPath); 103 104 Schema loadedSchema = factory.newSchema(schemaUrl); 105 logger.info("XML Schema loaded successfully from: {}", validatedPath); 106 securityLogger.info("Secure XML schema loading completed for: {}", validatedPath); 107 108 return loadedSchema; 109 110 } catch (SAXException | IllegalArgumentException e) { 111 logger.error("Failed to load XML schema from {}: {}", validatedPath, e.getMessage(), e); 112 securityLogger.error("Secure XML schema loading failed for: {}", validatedPath); 113 throw new StreamProcessingException( 114 String.format("XMLスキーマの読み込みに失敗しました - スキーマ: %s, エラー: %s", validatedPath, e.getMessage()), 115 e); 116 } 117 } 118 119 /** 120 * XMLのバリデーションを行うコマンドを実行します。 121 * 122 * <p>XMLのスキーマを指定して、XMLのバリデーションを行います。 123 * 124 * <p>バリデーションエラーが発生した場合は、エラーメッセージを出力します。 125 * 126 * @param inputStream 入力ストリーム 127 * @throws IOException 入出力エラーが発生した場合 128 * @throws StreamProcessingException XXE防止設定の適用失敗またはXMLバリデーションエラーが発生した場合 129 */ 130 @Override 131 public void consume(InputStream inputStream) throws IOException { 132 Objects.requireNonNull(inputStream, "Input stream cannot be null"); 133 134 try { 135 // セキュアなValidatorの作成 136 Validator validator = schema.newValidator(); 137 configureSecureValidator(validator); 138 139 // XMLバリデーションの実行 140 validator.validate(new StreamSource(inputStream)); 141 142 logger.info("XML validation completed successfully using schema: {}", schemaPath); 143 144 } catch (SAXException e) { 145 // バリデーションエラーの詳細ログ出力 146 logger.error("XMLバリデーションエラーが発生しました: {}", e.getMessage(), e); 147 148 // バリデーションエラーをカスタム例外でラップして伝播 149 throw new StreamProcessingException( 150 String.format("XMLバリデーションに失敗しました - スキーマ: %s, エラー: %s", schemaPath, e.getMessage()), e); 151 } 152 } 153 154 /** 155 * Validatorにセキュリティ設定を適用します 156 * 157 * @param validator 設定対象のValidator 158 * @throws SAXException セキュリティ設定に失敗した場合(XXE脆弱性のまま継続しないためスロー) 159 */ 160 private void configureSecureValidator(Validator validator) throws SAXException { 161 // XXE攻撃防止設定(設定失敗はXXE脆弱性のまま継続するため例外をスロー) 162 validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); 163 164 // 外部リソースアクセスを無効化 165 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 166 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 167 168 logger.debug("Secure XML processing features configured for Validator"); 169 } 170}