JavaScriptでURL判定を正規表現でやってはいけない

なぜ正規表現でURL判定してはいけないのか

JavaScriptでフォーム入力値のURLを検証したい場面が多くありますが、URL判定を正規表現だけで行うのは非常に危険です。

URLはRFC 3986およびWHATWG URL Standardで定義されており、とても複雑です。

  • スキーム(http: など)
  • ホスト(example.com)
  • 認証情報(user:pass@)
  • ポート番号
  • クエリ文字列
  • フラグメント
  • IPv4 / IPv6
  • IDN(日本語ドメイン)
  • etc.

そのため、これらをすべて正規表現で判定するのはほぼ不可能で、できたとしても、かなり複雑なコードになってしまいます。

セキュリティ的に危険な正規表現による判定

次のような正規表現によるURL判定のコードをWeb上でよく見かけます。

JavaScript
const isUrl = (url) => /^https?:\/\/.+$/.test(url)

console.log(isUrl('https://iwb.jp/')) // true
console.log(isUrl('iwb.jp')) // false

しかし、このようなコードは以下のような明らかにURLではない文字列もURLとして判定してしまうため、この判定だと問題が生じる可能性があります。

JavaScript
const isUrl = (url) => /^https?:\/\/.+$/.test(url)

console.log(isUrl(' https://iwb.jp/ ')) // false
console.log(isUrl('https:// ')) // true
console.log(isUrl('https://<script>')) // true
console.log(isUrl('https://テスト')) // true
console.log(isUrl('https://localhost:99999')) // true

URL判定はURLコンストラクタの使用を推奨

URL判定にはURLコンストラクタのnew URL()を使うことを推奨します。

以下のようにtry ... catch 文と併用することで、正確にURLを判定できます。

JavaScript
const isUrl = (url) => {
  try {
    const trimUrl = url.trim()
    const parsed = new URL(trimUrl)
    if (!/^[\u0000-\u007f]+$/.test(url)) return false
    return /https?:/.test(parsed.protocol)
  } catch {
    return false
  }
}

console.log(isUrl(' https://iwb.jp/ ')) // true
console.log(isUrl('https:// ')) // false
console.log(isUrl('https://<script>')) // false
console.log(isUrl('https://テスト')) // false
console.log(isUrl('https://localhost:99999')) // false

URLは前後にホワイトスペースがあっても「location.href = ' https://iwb.jp/ '」などのコードは通るので、JavaScriptの処理に使用するURLであれば trim() で除去しておいたほうが良いです。

また、「https://テスト」英語以外のドメインなどは通常はfalseにしたいケースが多いので、URLをASCIIのみか ^[\u0000-\007f]+$ の正規表現で判定しています。