【JavaScript】テキスト内のURLをリンクに置き換える方法|XSS対策・末尾句読点・target/relまで安全に実装

投稿フォームやコメント欄のテキストに含まれるURLを、自動でクリックできるリンク(aタグ)に変換したい場面はよくあります。JavaScriptで実装できますが、やり方を誤るとXSS(クロスサイトスクリプティング)の穴になるため、安全な手順を押さえることが重要です。

この記事の結論(安全な手順):先にHTMLエスケープしてXSSを防ぐ → ②正規表現でURLを検出(HTMLの危険文字や句読点は含めない) → ③target="_blank"rel="noopener noreferrer" を付けてリンク化。本番では実績のあるライブラリを使うのも有力です。
スポンサーリンク

まずNG例:そのまま innerHTML に入れると危険

よく見かける素朴な実装には、重大な問題があります。

NG: XSSと誤変換を起こす書き方
function replaceURLWithLink(text) {
  const urlRegex = /(https?:\/\/[^\s]+)/g; // [^\s]+ が " も飲み込む
  return text.replace(urlRegex, (url) => `<a href="${url}">${url}</a>`);
}

const div = document.getElementById("content");
div.innerHTML = replaceURLWithLink(div.innerHTML); // エスケープなしで危険
何が危険か:[^\s]+ は空白以外をすべて飲み込むため、https://x.com"onmouseover="alert(1) のような文字列でonmouseover 属性が注入されてXSSが発火します。さらに https://example.com。 の「。」や (https://example.com) の「)」までリンクに含まれ、innerHTML を読み直す処理は既存の <a> を壊します。

安全な実装:エスケープ → 検出 → リンク化

ポイントは「リンク化する前にテキストをHTMLエスケープする」ことです。これで、URL以外の部分にタグや属性が紛れ込んでも無害化されます。そのうえで、HTMLの危険文字や日本語の句読点を含まない正規表現でURLを検出します。

安全な linkify 関数
// 1) HTMLエスケープ(XSS対策の要)
function escapeHtml(s) {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function linkify(text) {
  const escaped = escapeHtml(text);
  // 空白・HTML危険文字・日本語句読点/括弧は URL に含めない
  const urlRegex = /https?:\/\/[^\s<>"。、)」]+/g;

  return escaped.replace(urlRegex, (url) => {
    // 末尾の半角句読点・閉じ括弧はリンクの外に出す
    const trail = (url.match(/[.,!?)\]]+$/) || [""])[0];
    const link = url.slice(0, url.length - trail.length);
    return `<a href="${link}" target="_blank" rel="noopener noreferrer">${link}</a>${trail}`;
  });
}
使い方(textContent から読む)
const div = document.getElementById("content");

// innerHTML ではなく textContent から「生のテキスト」を取得して変換
div.innerHTML = linkify(div.textContent);
innerHTML ではなく textContent から読むのがコツです。生のテキストを取得してからエスケープ&リンク化するため、すでにある <a> タグを壊したり二重リンクにしたりしませんtextContentinnerHTML の違いはHTML要素のテキストを変更する方法も参考になります。

動作例

上記の linkify() に渡したときの出力です。

入力と出力
linkify('Google: https://www.google.com です');
// → Google: <a href="https://www.google.com" target="_blank" rel="noopener noreferrer">https://www.google.com</a> です

linkify('詳細は https://example.com。続き');
// → 詳細は <a href="..." ...>https://example.com</a>。続き(「。」はリンク外)

linkify('攻撃 https://x.com"onmouseover="alert(1)');
// → " が &quot; にエスケープされ、属性は注入されない(XSS無効化)

本番では専用ライブラリも検討する

正規表現による自動リンク化は、国際化ドメイン・末尾記号・メールアドレス・www. 始まりなど考慮すべきケースが多く、完璧に作り込むのは大変です。ユーザー生成コンテンツを大量に扱うなら、実績のあるライブラリ(linkify-string / linkifyjs など)を使うほうが安全で確実です。

自作する場合の鉄則:hrefjavascript: スキームが入らないよう、https?://始まるものだけをリンク化してください。今回の正規表現は https?:// 限定なのでこの点は満たしています。

よくある質問(FAQ)

Qテキスト内のURLを自動でリンクに変換するには?
A①テキストをHTMLエスケープ → ②正規表現でURLを検出 → ③aタグに置換、の順で実装します。エスケープを先に行うことでXSSを防げます。置換時に target="_blank"rel="noopener noreferrer" を付けると安全です。
Qなぜ先にエスケープが必要なのですか?
Aエスケープしないと、テキストに含まれる <script> や属性がそのままHTMLとして解釈され、XSSの原因になります。先に <" を実体参照へ変換しておけば、URL以外の部分が悪用されることを防げます。
QURLの後ろの句読点までリンクになってしまいます。
A[^\s]+ のような正規表現は空白以外をすべて含むのが原因です。文字クラスから句読点や括弧を除外し(例:[^\s<>"。、)」]+)、さらにマッチ後に末尾の . , ) ! などをリンクの外へ出すと自然になります。
Q本番のサービスでも自作の正規表現で大丈夫ですか?
A小規模なら問題ありませんが、国際化ドメインやメールアドレス、www. 始まりなど考慮点が多いため、大量のユーザー入力を扱うなら linkifyjs などのライブラリが安全で確実です。

まとめ

テキスト内のURLを安全にリンク化する手順を整理します。

  • 先にHTMLエスケープしてXSSを防ぐ(最重要)
  • 正規表現は HTML危険文字・句読点を含めない[^\s<>"。、)」]+
  • 末尾の . ) などはリンクの外へ出す
  • target="_blank"rel="noopener noreferrer" を付ける
  • innerHTML ではなく textContent から読む(既存タグを壊さない)
  • 大規模なら ライブラリを検討、https?:// 限定で javascript: を排除

関連として、HTML要素のテキストを変更する方法(textContent・innerHTML・XSS対策)特定のクラス内の要素をアンカーリンクで囲む方法もあわせて読むと、安全なDOM操作に強くなれます。