SVG バリデーター

SVG を XML として検証し、実行可能な内容を検出。描画はしません。

入力
待機中文書を貼り付けると検査します。入力中にそのまま検証されます。

すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.

上に SVG を貼り付けるか、ファイルをエディターにドロップしてください。XML として検査し、構文エラーを一度にすべて、行・列・直し方つきで示します。SVG は XML の語彙ですから、ビルドツールやアイコンのパイプライン、ブラウザーに拒まれる SVG は、たいてい SVG の規則ではなく XML の規則に違反しています。

Figma や Illustrator、Inkscape からの書き出しがバンドラーを壊すとき、アイコンが真っ白で、そもそもファイルが解析できるのかを知りたいとき、あるいは利用者がアップロードした SVG をブラウザーに近づける前に読みたいとき、この道具を使ってください。

ここでの違いは、このページがやらないと決めていることにあります。たいていの SVG バリデーターはソースの隣にあなたのファイルを描画し、中のスクリプトを走らせうるのと同じ描画エンジンに渡します。このページは決して描画しません。あなたのマークアップはエディターの中の文字のままです。ページが表示する値はすべて textContent か createElement で書き込むので、ファイルのどの部分も innerHTML には届きませんし、何もアップロードされません。

SVG は、ブラウザーが実行する XML

SVG は <script> 要素を持てますし、どの図形にも onload や onmouseover 属性を付けられますし、javascript: で始まる href も、任意の HTML を抱えた <foreignObject> も持てます。いずれも正当な SVG であり、いずれも整形式の XML です。だからこそ、構文検査だけでは「このファイルは安全だ」とは決して言えません。

その内容がいつ走るかは、ファイルの使われ方によります。<img> タグや CSS 背景として読み込まれた SVG はスクリプトを実行せず、周囲のページにも手が届きません。あなたの DOM に直接埋め込まれたり、自分の URL で開かれたりすれば、配信した生成元の権限で実行されます。後者の場合、アップロード機能が蓄積型クロスサイトスクリプティングに変わります。

ですからこのページは XML を検査し、そのファイルを走らせる当の存在になることを断ります。<script> 要素は構文エラーとしては報告しません。実際にエラーではないからです。あなたが受け取るのは、整形式性の問題がすべて印された色つきのテキストと、実行可能コンテンツの検査を本来あるべき場所、つまりビルド手順に置くための下の監査コードです。

XML の失敗である SVG の失敗

パーサーが拒む SVG のほぼすべてがこれらで説明できます。それぞれ「SVG が不正です」の一言ではなく、正確な位置つきで報告します。

  • xmlns:xlink="http://www.w3.org/1999/xlink" が有効範囲にないまま xlink:href を使っている。束縛されていない接頭辞は Namespaces in XML における致命的エラーで、スプライトシートから切り出した断片で絶えず現れます。
  • <path>、<image>、<use>、<stop> が閉じられていない。HTML には空要素がありますが XML にはないので、<path d="..."> は閉じスラッシュがなければ未閉鎖タグであり、エラーはずっと後、対応に失敗したタグの位置で表面化します。
  • xlink:href のクエリ文字列の中の裸のアンパサンド。属性値の中では &amp; と書かねばなりません。あるいは style 属性の中のリテラルの <。> は許されるのに、XML はそこでの < を禁じます。
  • 1 つの要素に同じ属性が 2 回。変換をうまく併合できなかったとき、一部の書き出しパイプラインが作ってしまいます。
  • SVG 1.1 の DTD を指す古い Illustrator の DOCTYPE。報告はしますが決して取得しません。ここで何もその DTD に照らして検証しない理由でもあります。
  • 数百メガバイトに膨らむ内部実体サブセット。アップロードフォームが受け取ってしまうので、SVG はこの攻撃の運び手としてよく使われます。費用は宣言から計算し、ファイルは 2 ミリ秒ほどで拒否します。

整形式性の検査では分からないこと

ここをはっきり書くほうが、緑のチェックマークより役に立ちます。以下のファイルはどれも整形式の XML で、ここは通り、それでも SVG としては壊れています。

  • ルート要素に xmlns="http://www.w3.org/2000/svg" がない。HTML に直接書けばたいてい描画されます。HTML パーサーが SVG 名前空間を仮定するからです。しかし <img> 経由や image/svg+xml として配信すると何も描かれません。「SVG が真っ白」のいちばん多い原因です。
  • viewBox ではなく viewbox。XML の属性名は大文字小文字を区別し、SVG はキャメルケースの綴りしか定義していないので、ブラウザーは綴り違いを無視し、失敗するのではなくアイコンの拡大縮小がおかしくなります。
  • viewBox なしで width と height だけを設定すると、拡大縮小しない画像になります。幅か高さが 0 の viewBox は、何も描きません。
  • グラデーションやフィルター、<use> の参照のために他のファイルを指す xlink:href。ファイルが自分のディスクにある間は解決しますが、SVG を直接埋め込んだり別の生成元から配信したりした瞬間に壊れます。デザイナーのマシンにしかないフォントで組まれた <text> 要素も同じです。

アップロードされた SVG がブラウザーに届く前に

そのファイルが利用者から来たものなら、ここで読むのは最後の一歩ではなく最初の一歩だと考えてください。script という語での照合は防御になりません。ペイロードは base64 のデータ URI に、<set attributeName="onload"> に、animate 要素に、そして素朴なフィルターが見落とす名前空間の小細工に隠れます。

頼れる選択肢は、文字列検索ではなく解析を行うサニタイザー(Node やブラウザーでの DOMPurify の SVG プロファイル、PHP での enshrined/svg-sanitize)か、サーバー側で PNG にラスタライズすることです。どうしても原本を配信するなら、Content-Security-Policy を設定した別の生成元から配信し、直接埋め込まずに <img> 経由で参照してください。このページは報告するだけで、掃除はしません。

コードで SVG を監査する

まずファイルを安全に解析し、それからブラウザーが実行しうるものを探して歩きます。下のサンプルはどれも、最初に実体解決とネットワークアクセスを無効にします。DOCTYPE を抱えた SVG は、普通の XML 文書とまったく同じ意味で XXE のペイロードだからです。

// Parses the SVG and lists what a browser could execute. Nothing here
// inserts the markup into the page, which is what keeps it safe to run
// against a file you do not trust.
const EXECUTABLE = ['javascript:', 'vbscript:', 'data:text/html'];

function auditSvg(source) {
  const doc = new DOMParser().parseFromString(source, 'image/svg+xml');
  if (doc.querySelector('parsererror')) {
    return { wellFormed: false, findings: [] };
  }

  const findings = [];
  const walk = (node) => {
    const tag = node.localName.toLowerCase();
    if (tag === 'script' || tag === 'handler' || tag === 'foreignobject') {
      findings.push(node.nodeName + ' element');
    }
    for (const attr of node.attributes) {
      const value = attr.value.trim().toLowerCase();
      if (attr.name.toLowerCase().startsWith('on')) {
        findings.push(attr.name + ' event handler');
      }
      if (EXECUTABLE.some((scheme) => value.startsWith(scheme))) {
        findings.push('executable URI in ' + attr.name);
      }
    }
    for (const child of node.children) walk(child);
  };

  walk(doc.documentElement);
  return { wellFormed: true, findings };
}
# resolve_entities and no_network are the flags that matter: an SVG with a
# DOCTYPE is an XXE vector like any other XML document.
from lxml import etree

DANGEROUS = {'script', 'handler', 'foreignObject'}
EXECUTABLE = ('javascript:', 'vbscript:', 'data:text/html')

parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    load_dtd=False,
    huge_tree=False,
)
root = etree.fromstring(svg_bytes, parser)   # XMLSyntaxError if malformed

for el in root.iter():
    if not isinstance(el.tag, str):
        continue                             # comment or processing instruction
    if etree.QName(el).localname in DANGEROUS:
        print(el.sourceline, etree.QName(el).localname, 'element')
    for name, value in el.attrib.items():
        local = etree.QName(name).localname if name.startswith('{') else name
        if local.lower().startswith('on'):
            print(el.sourceline, name, 'event handler')
        if value.strip().lower().startswith(EXECUTABLE):
            print(el.sourceline, name, 'executable URI')
import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import java.util.Locale;
import java.util.Set;

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Old Illustrator exports carry a DOCTYPE. Drop the next line only if you
// must accept them, and keep the two features above false either way.
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

final Set<String> dangerous = Set.of("script", "handler", "foreignObject");

factory.newSAXParser().parse(
    new InputSource(new java.io.StringReader(svg)),
    new DefaultHandler() {
        @Override
        public void startElement(String uri, String local, String qName, Attributes atts) {
            if (dangerous.contains(local)) {
                System.out.println("element: " + qName);
            }
            for (int i = 0; i < atts.getLength(); i++) {
                String name = atts.getQName(i);
                String value = atts.getValue(i).trim().toLowerCase(Locale.ROOT);
                if (name.toLowerCase(Locale.ROOT).startsWith("on")) {
                    System.out.println("event handler: " + name);
                }
                if (value.startsWith("javascript:") || value.startsWith("data:text/html")) {
                    System.out.println("executable URI in " + name);
                }
            }
        }
    });
using System;
using System.Xml;

var dangerous = new[] { "script", "handler", "foreignObject" };
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,  // no DOCTYPE, no entity expansion
    XmlResolver = null,                      // never fetch anything
    MaxCharactersFromEntities = 1024 * 1024,
};

using var reader = XmlReader.Create(new StringReader(svg), settings);
var lineInfo = (IXmlLineInfo)reader;

while (reader.Read())
{
    if (reader.NodeType != XmlNodeType.Element) continue;

    if (Array.IndexOf(dangerous, reader.LocalName) >= 0)
        Console.WriteLine($"line {lineInfo.LineNumber}: <{reader.Name}>");

    while (reader.MoveToNextAttribute())
    {
        var value = reader.Value.Trim().ToLowerInvariant();
        if (reader.LocalName.StartsWith("on", StringComparison.OrdinalIgnoreCase))
            Console.WriteLine($"line {lineInfo.LineNumber}: {reader.Name} handler");
        if (value.StartsWith("javascript:") || value.StartsWith("data:text/html"))
            Console.WriteLine($"line {lineInfo.LineNumber}: URI in {reader.Name}");
    }
    reader.MoveToElement();
}
<?php
// LIBXML_NONET blocks every external fetch, including a DOCTYPE's DTD.
libxml_use_internal_errors(true);

$doc = new DOMDocument();
if (!$doc->loadXML($svg, LIBXML_NONET)) {
    fwrite(STDERR, "not well-formed XML\n");
    exit(1);
}

$xpath = new DOMXPath($doc);
$xpath->registerNamespace('svg', 'http://www.w3.org/2000/svg');

foreach ($xpath->query('//svg:script | //svg:handler | //svg:foreignObject') as $node) {
    printf("%s element on line %d\n", $node->nodeName, $node->getLineNo());
}

foreach ($xpath->query('//@*') as $attr) {
    $name = strtolower($attr->nodeName);
    $value = strtolower(trim($attr->nodeValue));
    if (str_starts_with($name, 'on') || str_starts_with($value, 'javascript:')) {
        printf("%s on line %d\n", $attr->nodeName, $attr->getLineNo());
    }
}
// To clean rather than report, use enshrined/svg-sanitize.
# Well-formedness first. An SVG that fails here fails as XML.
xmllint --noout --nonet icon.svg

# A smoke test for executable content. This is a grep, not a sanitiser: it
# misses base64 payloads, <set attributeName="onload">, and obfuscation.
grep -niE 'script|foreignObject|on[a-z]+ *=|javascript:|data:text/html' icon.svg

# Check a directory of exports and keep going after each failure:
find . -name '*.svg' -print0 | xargs -0 -n1 xmllint --noout --nonet

# svgo is a minifier and does not remove script by default.
# For sanitising, use DOMPurify in Node with its SVG profile.

タグ名や属性名の拒否リストは報告のための道具であって、セキュリティの境界ではありません。実際にブラウザーへ配信するものは、文書を解析して許可リストから組み直すサニタイザーを通すか、PNG にラスタライズすべきです。

よくある質問

私の SVG はアップロードされますか。どこかに表示されますか。

どちらもありません。パーサーはこのタブで動く JavaScript なので、ファイルが送信されることはありませんし、このページは意図的に描画しません。プレビュー枠もなく、あなたのマークアップを指す <img> もなく、innerHTML に届く経路もありません。

これは他のツールより、ここでこそ重要です。スクリプトを抱えた SVG は、何かが描画したときにはじめて危険になるからです。プレビューを見せるバリデーターは、信頼できないあなたのファイルを自分の生成元で走らせています。入力は、再読み込みで失われないようこのブラウザーの localStorage に保存します。これはあなたの機械に留まり、「クリア」ボタンで消え、30 万文字を超えるファイルでは行いません。

SVG ファイルに JavaScript を含められますか。

はい。SVG には <script> 要素があり、どの要素にも onload や onclick といったイベントハンドラー属性を置け、javascript: で始まる href 値も、埋め込み HTML のための <foreignObject> もあります。どれもファイルを不正にするものではなく、すべて形式の一部です。

それが走るかどうかは、ファイルの使われ方によります。<img> タグや CSS 背景経由ならスクリプトは実行されません。ページに直接埋め込んだり、自分の URL で開いたりすれば、配信した生成元の権限で実行されます。アバターのアップロードが蓄積型クロスサイトスクリプティングに変わるのがこの経路です。配信の前にサニタイズし、直接埋め込むより <img> を選んでください。

この検査は通るのに SVG が真っ白なのはなぜですか。

ほとんどの場合、名前空間が抜けています。ルート要素には xmlns="http://www.w3.org/2000/svg" が要ります。なくてもファイルは整形式の XML のままで、HTML に直接貼り付ければ HTML パーサーが名前空間を推測するのでたいてい描画されますが、<img> 経由や image/svg+xml として配信すると何も出ません。

次に多い原因は、viewbox と綴られた viewBox(XML の属性名は大文字小文字を区別するのでブラウザーは無視します)、幅か高さが 0 の viewBox、そして隣にもういないファイルのグラデーションへの参照です。どれも XML のエラーではないので、構文検査では報告されません。

これは SVG 仕様に照らして検証するのですか。

いいえ。そうでないふりをせず、はっきり書いておきます。検査するのは、ファイルが整形式の XML であることと、名前空間接頭辞が宣言されていることです。<circle> に r 属性があるか、path の d データが解析できるか、長さの値が正当かは見ません。

SVG 1.1 には DTD があり、ここの DTD バリデーターで文書をそれに照らすこともできますが、どのブラウザーもその DTD で SVG を検証しませんし、SVG 2 は DTD をまったく定義しておらず、完璧に描画されるファイルの多くが落ちます。整形式性の確認と、そのファイルが何を実行するのかを見ること。それが、SVG の実際の使われ方に合った検査です。

「接頭辞 xlink が名前空間に束縛されていません」とはどういう意味ですか。

ファイルのどこかで xlink:href を使っているのに、有効範囲に xmlns:xlink="http://www.w3.org/1999/xlink" の宣言がない、という意味です。接頭辞は束縛されるまで意味を持たないので、これは警告ではなく致命的エラーであり、適合するパーサーはどれもそのファイルを拒否します。

たいていは、スプライトシートから断片をコピーして、宣言を元のルートに置き去りにしたときに現れます。直し方は、手元のファイルのルート要素に宣言を足すことです。SVG 2 では接頭辞のいらない href 属性が xlink:href に取って代わり、現行のブラウザーは対応しているので、自分で管理しているファイルなら接頭辞をやめるほうが簡単なことも多いです。

関連ツール

関連する解説

これで解決できるエラー