セキュリティに関するブリーフィング

XML サービス拒否攻撃と防御策

Bryan Sullivan

サービス拒否 (DoS) 攻撃は、Web サイトに対する攻撃の中で、最も古くからある種類の攻撃の 1 つです。DoS 攻撃は、少なくとも 1992 年の記録には既に残っており、SQL インジェクション (1998 年に発見されました)、クロスサイト スクリプティング (JavaScript が発明されたのは 1995 年になってからです)、およびクロスサイト リクエスト フォージェリ (CSRF (クロスサイト リクエスト フォージェリ) 攻撃は一般にセッション Cookie を必要としますが、Cookie が世に出たのは 1994 年になってからです) よりも前から存在します。

当初から、DoS 攻撃はハッカーの間で非常に人気がありました。その理由は簡単で、最低限のスキルやリソースしか持っていない 1 人の "スクリプト キディー" レベルの攻撃者でも、サイトをサービス停止状態に追いやるのに十分な大量の TCP SYN (SYN は "同期" を意味します) 要求を生成することができるからです。新興の電子商取引界にとって、これは致命的でした。ユーザーはサイトにたどり着くことができなければ、サイトで十分にお金を使うこともできません。DoS 攻撃は、現実世界に置き換えて考えると、店の周りにレーザーワイヤーのフェンスを築くようなものです。ただし、現実世界の場合と違って、あらゆる店が昼夜を問わずいつでも攻撃される可能性があります。

長い年月をかけて、Web サーバー ソフトウェアやネットワーク ハードウェアの強化により、SYN フラッド攻撃は大幅に軽減されました。しかし、最近、セキュリティ コミュニティ内では、再び DoS 攻撃に関心が集まっています。関心が集まっているのは、昔からある、ネットワーク レベルの DoS ではなく、アプリケーション レベルの DoS (特に XML パーサー DoS) です。

XML DoS 攻撃は非常に非対称的です。攻撃者が攻撃ペイロードを与えるために費やす必要がある処理能力や帯域幅は、被害を受ける側がペイロードを処理するために費やす必要がある処理能力や帯域幅と比べるとほんの少しで済んでしまいます。さらに悪いことに、XML を処理するコード内の DoS 脆弱性は非常に広範囲に及びます。十分にテストされたパーサー (Microsoft .NET Framework の System.Xml クラスに用意されているパーサーなど) を使用している場合でも、明示的な保護措置を取らなければコードは依然として脆弱な可能性があります。

この記事では、新しい XML DoS 攻撃のいくつかについて説明します。また、潜在的な DoS 脆弱性を検出する方法、およびコード内でそれを軽減する方法も示します。

XML 爆弾

特に悪質な XML DoS 攻撃の一種が、XML 爆弾 (XML スキーマのルール上は適格かつ有効だが、プログラムがその XML ブロックを解析しようとするとそのプログラムをクラッシュまたはハングさせる XML ブロック) です。最も有名な XML 爆弾の例は、おそらく指数関数的エンティティ展開攻撃でしょう。

XML ドキュメント型定義 (DTD) 内には、独自のエンティティを定義することができます。こうしたエンティティは、実質的に、文字列置換マクロとして機能します。たとえば、DTD に次の行を追加して、&companyname; という文字列をすべて "Contoso Inc." に置き換えることができます。

<!ENTITY companyname "Contoso Inc.">

次のように、エンティティを入れ子にすることもできます。

<!ENTITY companyname "Contoso Inc.">
<!ENTITY divisionname "&companyname; Web Products Division">

ほとんどの開発者は外部の DTD ファイルを使用するのに慣れていますが、インライン DTD を XML データ自体と共に含めることも可能です。<!DOCTYPE> を使用して外部の DTD ファイルを参照するのではなく、次のように、単純に <!DOCTYPE > 宣言内で直接 DTD を定義します。

<?xml version="1.0"?>
<!DOCTYPE employees [
  <!ELEMENT employees (employee)*>
  <!ELEMENT employee (#PCDATA)>
  <!ENTITY companyname "Contoso Inc.">
  <!ENTITY divisionname "&companyname; Web Products Division">
]>
<employees>
  <employee>Glenn P, &divisionname;</employee>
  <employee>Dave L, &divisionname;</employee>
</employees>

今度は、攻撃者は、XML のこの 3 つの特性 (置換エンティティ、入れ子になったエンティティ、およびインライン DTD) を利用して、悪意のある XML 爆弾を作成することができます。攻撃者は、上記の例と同じような入れ子になったエンティティを含む XML ドキュメントを作成しますが、1 階層だけの入れ子にするのではなく、以下に示すように、エンティティを多重階層の入れ子にします。

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
  <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
  <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
  <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
  <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

この XML は、DTD のルール上は適格かつ有効です。XML パーサーによってこのドキュメントが読み込まれると、このドキュメントには "&lol9;" というテキストを含む 1 つのルート要素 ("lolz") が含まれていることが認識されます。しかし、"&lol9;" は定義済みのエンティティで、展開されると、10 個の "&lol8;" という文字列から成る文字列になります。"&lol8;" という文字列それぞれは、定義済みのエンティティで、展開されると、10 個の "&lol7;" という文字列になります。"&lol7;" およびそれ以降についても同様にお考えください。エンティティの展開がすべて処理されると、この小さな (1 KB 未満の) XML ブロックに実際のところ 10 億個の "lol" が含まれることになり、約 3 GB ものメモリが占有されます。次の非常に単純なコード ブロックを使用して、この攻撃 (Billion Laughs 攻撃と呼ばれることもあります) をご自分で試していただくこともできます。試す場合は、タスク マネージャーからテスト アプリケーション プロセスを強制終了する準備をしておいてください。

void processXml(string xml)
{
    System.Xml.XmlDocument document = new XmlDocument();
    document.LoadXml(xml);
}

ここで、悪知恵のはたらく読者の方の中には、次のように、互いを参照する 2 つのエンティティを使用して、エンティティの展開を無限に繰り返すことはできないかと思っている方がいらっしゃるかもしれません。

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol1 "&lol2;">
  <!ENTITY lol2 "&lol1;">
]>
<lolz>&lol1;</lolz>

これは可能であれば非常に効果的な攻撃となるでしょうが、さいわい、これは適格な XML ではないので解析されません。しかし、指数関数的エンティティ展開という XML 爆弾には、Trusteer の Amit Klein 氏によって発見された二次爆発攻撃というバリエーションもあり、この攻撃は機能します。この攻撃では、攻撃者は、深い入れ子になった小さなエンティティを複数定義するのではなく、次のように、非常に大きなエンティティを 1 つ定義し、それを何度も参照します。

<?xml version="1.0"?>
<!DOCTYPE kaboom [
  <!ENTITY a "aaaaaaaaaaaaaaaaaa...">
]>
<kaboom>&a;&a;&a;&a;&a;&a;&a;&a;&a;...</kaboom>

攻撃者が "&a;" というエンティティを長さ 50,000 文字として定義し、"kaboom" というルート要素内でこのエンティティを 50,000 回参照した場合、200 KB を少し超えるサイズの XML 爆弾攻撃ペイロードが、解析されると 2.5 GB にふくらみます。この膨張率は指数関数的エンティティ展開攻撃ほど顕著ではありませんが、解析処理を停止させるには十分です。

Klein 氏は、属性爆発攻撃という XML 爆弾も発見しました。古い XML パーサーの多く (.NET Framework Version 1.0 および 1.1 の XML パーサーを含む) は、非常に非効率的な二次 O(n2) ランタイムで XML 属性を解析します。1 つの要素に対して多数の (たとえば 100,000 個以上の) 属性が存在する XML ドキュメントを作成すると、XML パーサーはプロセッサを長時間独占するため、サービス拒否状態が発生します。ただし、この脆弱性は .NET Framework Version 2.0 およびそれ以降では修正されています。

外部エンティティ攻撃

エンティティ置換文字列を定数として定義するのではなく、次のように、こうした文字列の値を外部 URI から取得するように定義することもできます。

<!ENTITY stockprice SYSTEM    "https://www.contoso.com/currentstockprice.ashx">

厳密な動作は XML パーサーの具体的な実装によって異なりますが、このコードの趣旨は、"&stockprice;" というエンティティが検出されるたびに、www.contoso.com/currentstockprice.ashx に対して要求を行い、stockprice エンティティをこの要求に対する応答に置き換えるというものです。これは間違いなく XML の魅力的で便利な機能ですが、この機能によっていくつかの悪質な DoS 攻撃が可能になります。

外部エンティティ機能を悪用する最も簡単な方法は、XML パーサーを、呼び出し元に戻らせないリソースに (つまり、無限待機ループ内に) 差し向けることです。たとえば、攻撃者がサーバー adatum.com を制御できる場合、攻撃者は次のようなジェネリック HTTP ハンドラー ファイルを http://adatum.com/dos.ashx に用意することができます。

using System;
using System.Web;
using System.Threading;

public class DoS : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        Thread.Sleep(Timeout.Infinite);
    }

    public bool IsReusable { get { return false; } }
}

その後、攻撃者は、http://adatum.com/dos.ashx を参照する悪意のあるエンティティを作成することができ、XML パーサーが XML ファイルを読み取ると、パーサーはハングします。しかし、これはそれほど効果的な攻撃ではありません。DoS 攻撃の目的は、リソースを消費して、アプリケーションの正当なユーザーがリソースを使用できなくなるようにすることです。これより前に例として紹介した指数関数的エンティティ展開および二次爆発という XML 爆弾はサーバーに大量のメモリと CPU 時間を消費させますが、この例はそうではありません。この例によって実際に消費されるのは、1 つの実行スレッドのみです。次のように、サーバーにある程度のリソースを消費させるようにして、この攻撃を改良しましょう (攻撃者の視点から見た "改良" です)。

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType = "text/plain";
    byte[] data = new byte[1000000];
    for (int i = 0; i < data.Length; i++) { data[i] = (byte)’A’; }
    while (true)
    {
        context.Response.OutputStream.Write(data, 0, data.Length);
        context.Response.Flush();
    }
}

このコードでは、無数の 'A' という文字 (一度に 100 万個) を応答ストリームに書き込み、非常に短い時間で大量のメモリを消費します。攻撃者は、このために独自のページを用意することができない (または用意したくない) 場合 (おそらく、攻撃者は自分が犯人であることを示す証拠を残したくはないでしょう)、代わりに、外部エンティティの参照先を第三者の Web サイト上にある非常に大きなリソースにすることができます。これには、ムービーやファイル ダウンロードが特に効果的な場合があります。たとえば、Visual Studio 2010 Professional ベータ版ダウンロードは 2 GB を超えます。

この攻撃のもう 1 つの巧妙なバリエーションは、外部エンティティの参照先を対象サーバー自体のイントラネット リソースにするという方法です。この攻撃手法を発見したのは、Intel の Steve Orrin 氏です。この手法を使用するには、攻撃者はサーバーがアクセスできるイントラネット サイトについての内部知識を持っている必要がありますが、イントラネット リソース攻撃を実行できる場合、これは特に効果的です。対象サーバー自体のリソース (プロセッサ時間、帯域幅、およびメモリ) を使用して、そのサーバー自体、または同じネットワーク上にある他のサーバーを攻撃することになるからです。

XML 爆弾による攻撃を防ぐ

あらゆる種類の XML エンティティ攻撃を防ぐ最も簡単な方法は、単純に、XML 解析オブジェクト内でのインライン DTD スキーマの使用を完全に無効にすることです。これは、"使用していない機能は、攻撃者が悪用できないように無効にする" という攻撃対象領域削減の原則をそのまま適用したものです。

.NET Framework Version 3.5 およびそれ以前では、DTD 解析動作は、System.Xml.XmlTextReader クラスおよび System.Xml.XmlReaderSettings クラスにあるブール型の ProhibitDtd プロパティによって制御されます。インライン DTD を完全に無効にするには、このプロパティの値を true に設定します。これを行うには、次のようなコードを使用します。

XmlTextReader reader = new XmlTextReader(stream);
reader.ProhibitDtd = true;

または、次のようなコードを使用します。

XmlReaderSettings settings = new XmlReaderSettings();
settings.ProhibitDtd = true;
XmlReader reader = XmlReader.Create(stream, settings);

XmlReaderSettings クラスの ProhibitDtd プロパティの既定値は true ですが、XmlTextReader クラスの ProhibitDtd プロパティの既定値は false です。つまり、インライン DTD を無効にするには、このプロパティを明示的に true に設定する必要があるということです。

.NET Framework Version 4.0 (この記事を執筆している時点ではベータ版の段階) では、DTD 解析動作が変更されました。ProhibitDtd プロパティの使用は非推奨になり、代わりに新しい DtdProcessing プロパティの使用が推奨されるようになりました。次のように、このプロパティを Prohibit (既定値) に設定して、XML 内に <!DOCTYPE> 要素がある場合にランタイムによって例外がスローされるようにすることができます。

XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
XmlReader reader = XmlReader.Create(stream, settings);

DtdProcessing プロパティを Ignore に設定することもできます。こうすると、<!DOCTYPE> 要素が検出されても例外はスローされず、この要素は処理されず単純にスキップされます。また、インライン DTD を許可し処理する必要がある場合は、DtdProcessing を Parse に設定することもできます。

DTD の解析が本当に必要な場合は、コードを保護するためにいくつか追加の措置を取る必要があります。1 つ目の措置は、展開されたエンティティのサイズを制限することです。この記事で説明した攻撃は、展開されると非常に長い文字列になりパーサーに大量のメモリを消費させるエンティティを作成することによって機能することを思い出してください。XmlReaderSettings オブジェクトの MaxCharactersFromEntities プロパティを設定することにより、エンティティの展開を通じて作成できる文字の数に上限を設定することができます。妥当な上限を判断し、それに応じてロパティを設定してください。次に例を示します。

XmlReaderSettings settings = new XmlReaderSettings();
settings.ProhibitDtd = false;
settings.MaxCharactersFromEntities = 1024;
XmlReader reader = XmlReader.Create(stream, settings);

外部エンティティ攻撃を防ぐ

ここまでのところで、XML 爆弾に対する脆弱性が大幅に低下するようにこのコードを強化しました。しかし、悪意のある外部エンティティによってもたらされる脅威にはまだ対処していません。XmlResolver を変更して XmlReader の動作をカスタマイズすると、こうした攻撃に対する耐性を向上させることができます。XmlResolver オブジェクトは、外部エンティティを含む外部参照を解決するために使用されます。XmlTextReader インスタンス、および XmlReader.Create の呼び出しから返される XmlReader インスタンスには、既定の XmlResolver (実際は XmlUrlResolver) が設定されます。XmlReaderSettings の XmlResolver プロパティを null に設定すると、XmlReader がインライン エンティティを解決することは許可しつつ外部エンティティを解決することは防ぐことができます。ここでも、"不要な機能は無効にする" という攻撃対象領域削減の原則が適用されています。

XmlReaderSettings settings = new XmlReaderSettings();
settings.ProhibitDtd = false;
settings.MaxCharactersFromEntities = 1024;
settings.XmlResolver = null;
XmlReader reader = XmlReader.Create(stream, settings);

この状況が当てはまらないとしても (本当に、真に外部エンティティを解決する必要がある場合)、すべての希望が失われたわけではありません。ですが、少し追加の作業が必要になります。サービス拒否攻撃に対する XmlResolver の耐性を向上させるには、3 つの方法でその動作を変更する必要があります。第 1 に、無限遅延攻撃を防ぐために要求のタイムアウトを設定する必要があります。第 2 に、取得されるデータの量を制限する必要があります。第 3 に、徹底的な防御策として、XmlResolver がローカル ホスト上のリソースを取得することを禁止する必要があります。この 3 つはすべて、カスタムの XmlResolver クラスを作成することによって行うことができます。

変更を加える必要がある動作は、XmlResolver の GetEntity メソッドによって制御されます。次のように、XmlUrlResolver から派生する XmlSafeResolver という新しいクラスを作成し、GetEntity メソッドをオーバーライドします。

class XmlSafeResolver : XmlUrlResolver
{
    public override object GetEntity(Uri absoluteUri, string role, 
        Type ofObjectToReturn)
    {

    }
}

XmlUrlResolver.GetEntity メソッドの既定の動作は次のコードのような内容です。これを独自の実装の出発点として使用することができます。

public override object GetEntity(Uri absoluteUri, string role, 
    Type ofObjectToReturn)
{
    System.Net.WebRequest request = WebRequest.Create(absoluteUri);
    System.Net.WebResponse response = request.GetResponse();
    return response.GetResponseStream();
}

最初の変更は、要求を行う際および応答を読み取る際にタイムアウト値を適用することです。System.Net.WebRequest クラスと System.IO.Stream クラスはどちらも既定でタイムアウトをサポートしています。図 1 に示したサンプル コードでは、単純にタイムアウト値をハードコーディングしましたが、構成容易性を高める必要がある場合は、簡単に XmlSafeResolver クラスでパブリックの Timeout プロパティを公開することができます。

図 1 タイムアウト値を構成

private const int TIMEOUT = 10000;  // 10 seconds

public override object GetEntity(Uri absoluteUri, string role, 
   Type ofObjectToReturn)
{
    System.Net.WebRequest request = WebRequest.Create(absoluteUri);
    request.Timeout = TIMEOUT;

    System.Net.WebResponse response = request.GetResponse();
    if (response == null)
        throw new XmlException("Could not resolve external entity");

    Stream responseStream = response.GetResponseStream();
    if (responseStream == null)
        throw new XmlException("Could not resolve external entity");
    responseStream.ReadTimeout = TIMEOUT;
    return responseStream;
}

次は、応答で取得されるデータの量に上限を設定します。Stream クラスには "MaxSize" プロパティやそれに相当するものは存在しないので、この機能を自分で実装する必要があります。これを行うには、応答ストリームから一度に 1 チャンクずつデータを読み取り、それをローカル ストリーム キャッシュ内にコピーします。応答ストリームから読み取られたバイトの総数が定義済みの上限 (ここでも、単に単純化のためにハードコーディングされています) を超えた場合、ストリームからの読み取りを中断し、例外をスローします (図 2 参照)。

図 2 取得されるデータの量に上限を設定

private const int TIMEOUT = 10000;                   // 10 seconds
private const int BUFFER_SIZE = 1024;                // 1 KB 
private const int MAX_RESPONSE_SIZE = 1024 * 1024;   // 1 MB

public override object GetEntity(Uri absoluteUri, string role, 
   Type ofObjectToReturn)
{
    System.Net.WebRequest request = WebRequest.Create(absoluteUri);
    request.Timeout = TIMEOUT;

    System.Net.WebResponse response = request.GetResponse();
    if (response == null)
        throw new XmlException("Could not resolve external entity");

    Stream responseStream = response.GetResponseStream();
    if (responseStream == null)
        throw new XmlException("Could not resolve external entity");
    responseStream.ReadTimeout = TIMEOUT;

    MemoryStream copyStream = new MemoryStream();
    byte[] buffer = new byte[BUFFER_SIZE];
    int bytesRead = 0;
    int totalBytesRead = 0;
    do
    {
        bytesRead = responseStream.Read(buffer, 0, buffer.Length);
        totalBytesRead += bytesRead;
        if (totalBytesRead > MAX_RESPONSE_SIZE)
            throw new XmlException("Could not resolve external entity");
        copyStream.Write(buffer, 0, bytesRead);
    } while (bytesRead > 0);

    copyStream.Seek(0, SeekOrigin.Begin);
    return copyStream;
}

Stream クラスをラップし、オーバーライドされた Read メソッド内に上限チェックを直接実装するという方法もあります (図 3 参照)。こちらの方が、より効率的な実装です。前の例ではキャッシュされた MemoryStream 用に余分なメモリが確保されていましたが、今回はそれが不要だからです。

図 3 サイズが制限されたストリーム ラッパー クラスを定義

class LimitedStream : Stream
{
    private Stream stream = null;
    private int limit = 0;
    private int totalBytesRead = 0;

    public LimitedStream(Stream stream, int limit)
    {
        this.stream = stream;
        this.limit = limit;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        int bytesRead = this.stream.Read(buffer, offset, count);
        checked { this.totalBytesRead += bytesRead; }
        if (this.totalBytesRead > this.limit)
            throw new IOException("Limit exceeded");
        return bytesRead;
    }

    ...
}

今度は、単純に、WebResponse.GetResponseStream から返されたストリームを LimitedStream 内にラップし、GetEntity メソッドから LimitedStream を返します (図 4 参照)。

図 4 GetEntity で LimitedStream を使用

private const int TIMEOUT = 10000; // 10 seconds
private const int MAX_RESPONSE_SIZE = 1024 * 1024; // 1 MB

public override object GetEntity(Uri absoluteUri, string role, Type
ofObjectToReturn)
{
    System.Net.WebRequest request = WebRequest.Create(absoluteUri);
    request.Timeout = TIMEOUT;

    System.Net.WebResponse response = request.GetResponse();
    if (response == null)
        throw new XmlException("Could not resolve external entity");

    Stream responseStream = response.GetResponseStream();
    if (responseStream == null)
        throw new XmlException("Could not resolve external entity");
    responseStream.ReadTimeout = TIMEOUT;

    return new LimitedStream(responseStream, MAX_RESPONSE_SIZE);
}

最後に、徹底的な防御策をもう 1 つ追加します。解決されるとローカル ホストになる URI のエンティティ解決を阻止するのです (図 5 参照)。このような URI には、http://localhost、http://127.0.0.1、file:// のいずれかで始まる URI が含まれます。これにより、非常に悪質な情報公開の脆弱性 (攻撃者は file:// リソースを参照するエンティティを作成することができ、その後、こうしたリソースのコンテンツがパーサーによって問題なく取得され XML ドキュメント内に書き込まれる) も防止されます。

図 5 ローカル ホストのエンティティ解決を阻止

public override object GetEntity(Uri absoluteUri, string role,
    Type ofObjectToReturn)
{
    if (absoluteUri.IsLoopback)
        return null;
    ...
}

より安全な XmlResolver を定義したので、これを XmlReader に適用する必要があります。次のように、XmlReaderSettings オブジェクトを明示的にインスタンス化し、XmlResolver プロパティを XmlSafeResolver のインスタンスに設定し、XmlReader を作成する際は XmlReaderSettings を使用します。

XmlReaderSettings settings = new XmlReaderSettings();
settings.XmlResolver = new XmlSafeResolver();
settings.ProhibitDtd = false;   // comment out if .NET 4.0 or later
settings.DtdProcessing = DtdProcessing.Parse;  // comment out if 
                                               // .NET 3.5 or earlier
settings.MaxCharactersFromEntities = 1024;
XmlReader reader = XmlReader.Create(stream, settings);

その他の考慮事項

System.Xml のクラスの多くでは、オブジェクトやメソッドに XmlReader が明示的に提供されない場合、そのオブジェクト/メソッド用にフレームワーク コード内で XmlReader が暗黙的に作成されることに注意してください。この暗黙的に作成された XmlReader は、この記事で説明した追加の防御策をどれも持たず、攻撃に対して脆弱です。この記事の最初のコード スニペット (以下に示します) がこの動作の良い例です。

void processXml(string xml)
{
    System.Xml.XmlDocument document = new XmlDocument();
    document.LoadXml(xml);
}

このコードは、この記事で説明したすべての攻撃に対して完全に脆弱です。このコードを改良するには、適切な設定で (インライン DTD の解析を無効にするか、より安全なリゾルバー クラスを指定します) XmlReader を明示的に作成し、XmlDocument.LoadXml や別の XmlDocument.Load オーバーロードではなく XmlDocument.Load(XmlReader) オーバーロードを使用します (図 6 参照)。

図 6 より安全なエンティティ解析設定を XmlDocument に適用

void processXml(string xml)
{
    MemoryStream stream =
        new MemoryStream(Encoding.Default.GetBytes(xml));
    XmlReaderSettings settings = new XmlReaderSettings();

    // allow entity parsing but do so more safely
    settings.ProhibitDtd = false;
    settings.MaxCharactersFromEntities = 1024;
    settings.XmlResolver = new XmlSafeResolver();

    XmlReader reader = XmlReader.Create(stream, settings);
    XmlDocument doc = new XmlDocument();
    doc.Load(reader);
}

XLinq の方が、既定の設定では若干安全です。System.Xml.Linq.XDocument 用に既定で作成される XmlReader は DTD 解析を許可しますが、MaxCharactersFromEntities を自動的に 10,000,000 に設定し、外部エンティティの解決を禁止します。明示的に XDocument に XmlReader を提供する場合は、前述の防御設定を適用するようにしてください。

まとめ

XML エンティティの展開は強力な機能ですが、アプリケーションへのサービスを拒否するために攻撃者が簡単に悪用できてしまいます。攻撃対象領域削減の原則に従い、使用する必要がない場合はエンティティの展開を無効にするようにしてください。無効にしない場合は、アプリケーションがエンティティの展開に費やすことができる時間とメモリの量に上限を設定するための適切な防御策を適用してください。  

Bryan Sullivan は、マイクロソフトのセキュリティ開発ライフサイクル チームのセキュリティ プログラム マネージャーであり、Web アプリケーションと .NET のセキュリティ問題を専門に扱っています。『Ajax セキュリティ』(毎日コミュニケーションズ、2008 年) の著者でもあります。