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}