XXEとJava
XXEとは
XXE(XML External Entity)とは、XMLの外部参照機能を利用して、サーバ内部のファイル内容を取得(漏えい)したり、内部ネットワーク上のファイルにアクセスしたりする不正行為
XXEは、XMLを扱うアプリケーションで発現する可能性があるので、XML文書を取り扱う際には注意する必要がある。
JavaとXML
JavaでXMLを扱うには、javax.xml.parsers.DocumentBuilderを利用が考えられる。
よって、以降は、javax.xml.parsers.DocumentBuilderを使って、再現させたり、対策を考えてみたりしよう。
結論
XXE基本編のリンクには、JavaVMのプロパティで制御する方法もあるので、手っ取り早くはそちらかもしれない。
その一方で、Javaプログラマであれば、リゾルバを自作して制御することも難しくはないはず。
JavaでXXEを発現させる
下記のソースコードのプログラムは、第一引数にXMLファイル、第二引数に外部参照の動作モードを指定するようになっているので、既定の動作モードで、XMLファイル(C:\z\xmlxxe\in1.xml)を読んでみる。
こんな感じ。
Java の場合は、既定ではXXE攻撃を受けてしまうということになる。
外部参照のオブジェクトをNULLにする
次はjavax.xml.parsers.DocumentBuilder#setEntityResolver()メソッドで「NULL」を与えてみる。
こんな感じ。
.NET Framework とは異なり、Nullは既定のリゾルバオブジェクトということになるので、Java の場合は、NULLでもXXE攻撃を受けてしまうということになる。
外部参照のオブジェクトを自作する
次は「外部参照機能を限定的に使いたいけど、XXEを防ぎたい」。ていうか「リゾルバの修正で外部参照機能を防ぎたい」という場合は、リゾルバを自作すればいい。
つまり、javax.xml.parsers.DocumentBuilder#setEntityResolver()メソッドの第一引数は「org.xml.sax.EntityResolver」インターフェイス型なので、それを継承したクラスを自作して、それをjavax.xml.parsers.DocumentBuilder#setEntityResolver()メソッドの第一引数に割り当てれば、外部参照を自由に制御する事ができる。
上書き必須のメソッドは、「org.xml.sax.InputSource.InputSource resolveEntity(String publicId,String systemId)」メソッドだけ。
これの肝になる部分は、第二引数に外部参照のURLを示すString型が来るので、それに応じた「org.xml.sax.InputSource.InputSource」型を返せばいいだけ。
そして、この「org.xml.sax.InputSource.InputSource」型からの適切な「java.io.InputStream」クラス、または「java.io.Reader」クラスを返すようにしてあげればよい。
例えば、認証が必要だったり、特定のURIだけ許可したり、というような個別カスタマイズが可能。
ソースコードの「exEntityResolver.java」の「exEntityResolverクラス」は、全てのURIを示すsystemIdに対して、最終的に空のjava.io.ByteArrayInputStreamを返すだけの「exInputSource」クラスを返すことにした。
こんな感じになる。
ソースコード(XXEtest.java)
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import java.io.File;
import org.xml.sax.EntityResolver;
public class XXEtest{
public static void main(String args[]){
System.out.println("java.exe test <<inFile>> <<Resolver>>");
if(0 < args.length){
Boolean IsResolve = false;
exEntityResolver myExEntityResolver = null;
if(1 < args.length){
if(args[1].equals("null") == true){
IsResolve = true;
}else{
myExEntityResolver = new exEntityResolver();
}
}
try{
DocumentBuilderFactory tempDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder tempDocumentBuilder = tempDocumentBuilderFactory.newDocumentBuilder();
if(IsResolve == true){
tempDocumentBuilder.setEntityResolver(null);
}else if(myExEntityResolver != null){
tempDocumentBuilder.setEntityResolver(myExEntityResolver);
}
Document tempDocument = tempDocumentBuilder.parse(new File(args[0]));
System.out.println("getTextContent : " + tempDocument.getTextContent());
System.out.print("getXmlStandalone: ");
tempDocument.setXmlStandalone(true);
System.out.println(tempDocument.getXmlStandalone());
System.out.println("========Detail=================");
PrintNode(tempDocument.getChildNodes(), "");
}catch(Exception e){
e.printStackTrace();
}
}
}
static void PrintNode(NodeList nodeList, String spacer){
Node node = null;
for(int i=0; i< nodeList.getLength(); i++){
node = nodeList.item(i);
String typeStr = "unknown";
switch(node.getNodeType()){
case Node.ELEMENT_NODE:
typeStr = "ELEMENT_NODE";
break;
case Node.ATTRIBUTE_NODE:
typeStr = "ATTRIBUTE_NODE";
break;
case Node.TEXT_NODE:
typeStr = "TEXT_NODE";
break;
case Node.CDATA_SECTION_NODE:
typeStr = "CDATA_SECTION_NODE";
break;
case Node.ENTITY_REFERENCE_NODE:
typeStr = "ENTITY_REFERENCE_NODE";
break;
case Node.ENTITY_NODE:
typeStr = "ENTITY_NODE";
break;
case Node.PROCESSING_INSTRUCTION_NODE:
typeStr = "PROCESSING_INSTRUCTION_NODE";
break;
case Node.COMMENT_NODE:
typeStr = "COMMENT_NODE";
break;
case Node.DOCUMENT_NODE:
typeStr = "DOCUMENT_NODE";
break;
case Node.DOCUMENT_TYPE_NODE:
typeStr = "DOCUMENT_TYPE_NODE";
break;
case Node.NOTATION_NODE:
typeStr = "NOTATION_NODE";
break;
}
System.out.println(spacer + "name =" + node.getNodeName() + ", type=" + typeStr);
System.out.println(spacer + "value=" + node.getNodeValue());
System.out.println(spacer + "getTextContent=" + node.getTextContent());
if(node.hasChildNodes() == true){
PrintNode(node.getChildNodes(), spacer + " ");
}
}
}
}
ソースコード(exEntityResolver.java)
空の ByteArrayInputStream を返すだけの InputSource クラスを返すだけのリゾルバ
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;
public class exEntityResolver implements EntityResolver{
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException{
System.out.println("publicId: " + publicId);
System.out.println("systemId: " + systemId);
return (InputSource)new exInputSource();
}
}
ソースコード(exInputSource.java)
空の ByteArrayInputStream を返すだけの InputSource クラス。
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.Reader;
import java.io.InputStreamReader;
public class exInputSource extends InputSource{
ByteArrayInputStream myStream;
public exInputSource(){
byte[] hako = new byte[0];
this.myStream = new ByteArrayInputStream(hako);
}
public InputStream getByteStream(){
return (InputStream)this.myStream;
}
public Reader getCharacterStream(){
return (Reader)new InputStreamReader(this.myStream);
}
}