001package com.streamconverter.security;
002
003import java.io.InputStream;
004import javax.xml.XMLConstants;
005import javax.xml.parsers.DocumentBuilder;
006import javax.xml.parsers.DocumentBuilderFactory;
007import javax.xml.parsers.ParserConfigurationException;
008import javax.xml.stream.XMLInputFactory;
009import javax.xml.transform.TransformerFactory;
010import javax.xml.validation.SchemaFactory;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014/**
015 * XML処理のセキュリティ設定を提供するユーティリティクラス
016 *
017 * <p>このクラスは、XXE(XML外部エンティティ)攻撃や その他のXML関連のセキュリティ脆弱性を防ぐため、 安全なXML処理設定を提供します。
018 *
019 * <p>主な機能:
020 *
021 * <ul>
022 *   <li>XXE攻撃防止のためのDocumentBuilderFactory設定
023 *   <li>安全なXMLInputFactory設定
024 *   <li>安全なTransformerFactory設定
025 *   <li>安全なSchemaFactory設定
026 *   <li>セキュリティ設定の動的調整
027 * </ul>
028 *
029 * @since 1.0.0
030 */
031public class SecureXmlConfiguration {
032
033  private static final Logger logger = LoggerFactory.getLogger(SecureXmlConfiguration.class);
034  private static final Logger securityLogger =
035      LoggerFactory.getLogger("com.streamConverter.security");
036
037  private SecureXmlConfiguration() {
038    // ユーティリティクラスのため、インスタンス化を禁止
039  }
040
041  /**
042   * XXE攻撃を防ぐように設定されたDocumentBuilderFactoryを作成します
043   *
044   * @return 安全に設定されたDocumentBuilderFactory
045   * @throws ParserConfigurationException 設定に失敗した場合
046   */
047  public static DocumentBuilderFactory createSecureDocumentBuilderFactory()
048      throws ParserConfigurationException {
049    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
050
051    // XXE攻撃防止設定(設定失敗はXXE脆弱性のまま継続するため例外をスロー)
052    // DOCTYPE宣言を無効化
053    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
054    securityLogger.debug("DOCTYPE declarations disabled");
055
056    // 外部一般エンティティを無効化
057    factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
058    // 外部パラメータエンティティを無効化
059    factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
060    // 外部DTDロードを無効化
061    factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
062    securityLogger.debug("External entities disabled");
063
064    // JAXP標準の外部リソースアクセス制限
065    // setAttribute は未サポート属性時に IllegalArgumentException を投げる可能性があるため
066    // ParserConfigurationException にラップして fail-fast を維持する
067    try {
068      factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
069      factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
070    } catch (IllegalArgumentException e) {
071      ParserConfigurationException pce =
072          new ParserConfigurationException(
073              "Failed to configure secure XML external access attributes");
074      pce.initCause(e);
075      throw pce;
076    }
077
078    // 追加のセキュリティ設定
079    factory.setNamespaceAware(true);
080    factory.setValidating(false);
081
082    // XMLリーダーのセキュリティ制限
083    factory.setXIncludeAware(false);
084    factory.setExpandEntityReferences(false);
085
086    securityLogger.info("Secure DocumentBuilderFactory created with XXE protection");
087    return factory;
088  }
089
090  /**
091   * 安全に設定されたDocumentBuilderを作成します
092   *
093   * @return 安全に設定されたDocumentBuilder
094   * @throws ParserConfigurationException 設定に失敗した場合
095   */
096  public static DocumentBuilder createSecureDocumentBuilder() throws ParserConfigurationException {
097    DocumentBuilderFactory factory = createSecureDocumentBuilderFactory();
098    return factory.newDocumentBuilder();
099  }
100
101  /**
102   * XXE攻撃を防ぐように設定されたXMLInputFactoryを作成します
103   *
104   * @return 安全に設定されたXMLInputFactory
105   */
106  public static XMLInputFactory createSecureXMLInputFactory() {
107    XMLInputFactory factory = XMLInputFactory.newInstance();
108
109    // 外部エンティティの処理を無効化
110    factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
111    factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
112
113    // 追加のセキュリティ設定
114    factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
115    factory.setProperty(XMLInputFactory.IS_VALIDATING, false);
116
117    securityLogger.info("Secure XMLInputFactory created with XXE protection");
118    return factory;
119  }
120
121  /**
122   * 安全に設定されたTransformerFactoryを作成します
123   *
124   * <p>XXE攻撃防止設定(設定失敗はXXE脆弱性のまま継続するため例外をスロー)
125   *
126   * @return 安全に設定されたTransformerFactory
127   * @throws IllegalStateException セキュリティ設定の適用に失敗した場合
128   */
129  public static TransformerFactory createSecureTransformerFactory() {
130    TransformerFactory factory = TransformerFactory.newInstance();
131
132    try {
133      // XMLTransform攻撃を防ぐための設定
134      factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
135      factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
136      // 機能制限
137      factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
138    } catch (Exception e) {
139      throw new IllegalStateException(
140          "Failed to configure secure TransformerFactory: XXE protection could not be applied", e);
141    }
142
143    securityLogger.info("Secure TransformerFactory created with XXE protection");
144    return factory;
145  }
146
147  /**
148   * 安全に設定されたSchemaFactoryを作成します
149   *
150   * <p>XXE攻撃防止設定(設定失敗はXXE脆弱性のまま継続するため例外をスロー)
151   *
152   * @return 安全に設定されたSchemaFactory
153   * @throws IllegalStateException セキュリティ設定の適用に失敗した場合
154   */
155  public static SchemaFactory createSecureSchemaFactory() {
156    SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
157
158    try {
159      // 外部リソースアクセスを制限
160      factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
161      factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
162      // セキュアプロセシング機能を有効化
163      factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
164    } catch (Exception e) {
165      throw new IllegalStateException(
166          "Failed to configure secure SchemaFactory: XXE protection could not be applied", e);
167    }
168
169    securityLogger.info("Secure SchemaFactory created with XXE protection");
170    return factory;
171  }
172
173  /**
174   * ストリーム処理用に安全なDocumentBuilderを作成します。
175   *
176   * <p>引数の {@code inputStream} はnullチェックにのみ使用され、実際のXML解析はこのメソッド内では行いません。 返された {@code
177   * DocumentBuilder} を使って呼び出し元が解析を行います。
178   *
179   * @param inputStream nullチェック対象のInputStream(nullの場合は {@link IllegalArgumentException} をスロー)
180   * @return ストリーム処理用に安全に設定されたDocumentBuilder
181   * @throws ParserConfigurationException XML設定エラーが発生した場合
182   * @throws IllegalArgumentException inputStreamがnullの場合
183   */
184  public static DocumentBuilder createSecureDocumentBuilderForStream(InputStream inputStream)
185      throws ParserConfigurationException {
186
187    // 入力検証
188    if (inputStream == null) {
189      throw new IllegalArgumentException("InputStream cannot be null");
190    }
191
192    DocumentBuilder builder = createSecureDocumentBuilder();
193
194    // エラーハンドラーの設定(セキュリティ上の理由により詳細エラー情報を制限)
195    builder.setErrorHandler(new SecurityAwareErrorHandler());
196
197    securityLogger.debug("Secure DocumentBuilder created for InputStream processing");
198    return builder;
199  }
200}
201
202/**
203 * セキュリティを考慮したXMLエラーハンドラー。
204 *
205 * <p>詳細なエラー情報の外部漏洩を防ぐため、外部向けメッセージには一般的な内容のみを使用する。 内部ログには行・列情報を記録し、デバッグを可能にする。
206 *
207 * <p>このクラスは {@link
208 * SecureXmlConfiguration#createSecureDocumentBuilderForStream(java.io.InputStream)}
209 * 内部での使用を想定してpackage-privateとしており、外部から直接インスタンス化されることを想定していない。
210 */
211class SecurityAwareErrorHandler implements org.xml.sax.ErrorHandler {
212  private static final Logger logger = LoggerFactory.getLogger(SecurityAwareErrorHandler.class);
213  private static final Logger securityLogger =
214      LoggerFactory.getLogger("com.streamConverter.security");
215
216  /**
217   * XML解析の警告を処理する。
218   *
219   * <p>セキュリティ上の理由から詳細は抑制し、内部ロガーにはDEBUGレベルで一般的なメッセージのみを記録する。 セキュリティロガーにはWARNレベルで警告発生を記録する。
220   *
221   * @param exception 発生した警告の詳細(ログには詳細情報を含めない)
222   */
223  @Override
224  public void warning(org.xml.sax.SAXParseException exception) {
225    logger.debug("XML parsing warning (details suppressed for security)");
226    securityLogger.warn("XML parsing warning detected during secure processing");
227  }
228
229  /**
230   * XML解析の回復可能エラーを処理する。
231   *
232   * <p>セキュリティコンテキストでは回復可能エラーも失敗として扱う(不正XMLを後続処理に渡さない)。 内部ログには行・列情報を記録するが、外部エラーメッセージには詳細を含めない。
233   *
234   * @param exception 発生したエラーの詳細
235   * @throws org.xml.sax.SAXException 常にスローし、XML処理を中断する
236   */
237  @Override
238  public void error(org.xml.sax.SAXParseException exception) throws org.xml.sax.SAXException {
239    logger.warn(
240        "XML parsing error at line {}, column {}",
241        exception.getLineNumber(),
242        exception.getColumnNumber());
243    securityLogger.warn("XML parsing error detected during secure processing");
244    throw new org.xml.sax.SAXException("XML processing failed: parsing error detected", exception);
245  }
246
247  /**
248   * XML解析の致命的エラーを処理する。
249   *
250   * <p>セキュリティ制約違反とみなし、詳細情報を外部に漏らさずに処理を中断する。
251   *
252   * @param exception 発生した致命的エラーの詳細
253   * @throws org.xml.sax.SAXException 常にスローし、XML処理を中断する
254   */
255  @Override
256  public void fatalError(org.xml.sax.SAXParseException exception) throws org.xml.sax.SAXException {
257    securityLogger.error("Fatal XML parsing error detected during secure processing");
258    throw new org.xml.sax.SAXException("XML processing failed due to security constraints");
259  }
260}