URLエンコードとは何か

URLエンコード(パーセントエンコーディング)とは、URLに含められない文字を %XX 形式(XXはバイト値の16進数2桁)に変換する仕組みです。現行の標準仕様は RFC 3986(2005年)で、ブラウザ実装はこれと並立するWHATWGのURL Living Standardに準拠しています。

RFC 3986は文字を「非予約文字」と「予約文字」に分類します。非予約文字は A-Z a-z 0-9 - . _ ~ の66文字で、エンコードしてもしなくても同じURIとみなされます。予約文字はURIの区切りとして意味を持つため、データとして送る場合はエンコードが必須です。

予約文字と非予約文字の区別

RFC 3986 における文字の分類
分類文字役割
非予約 (unreserved)A-Z a-z 0-9 - . _ ~そのまま使える。正規化時はデコード推奨
予約: gen-delims: / ? # [ ] @スキームやパスなど大きな区切り
予約: sub-delims! $ & ' ( ) * + , ; =クエリ内などの細かい区切り
入力を1文字ずつ評価 非予約文字 A-Za-z0-9 - . _ ~ 予約文字 : / ? # & = など 非ASCII文字 日本語・絵文字など そのまま出力 データなら %XX に 区切りならそのまま UTF-8 バイト列へ 各バイトを %XX 例: あ (U+3042) → E3 81 82 → %E3%81%82
図1: パーセントエンコーディングの判定フロー。非ASCII文字はUTF-8に変換後、各バイトを個別にエンコードする。

JavaScriptの3つのエンコード関数

JavaScriptには用途の異なるエンコード関数が複数あり、選択を誤ると区切り文字が壊れてバグになります。URLエンコード/デコードツールで実際の出力を見比べると違いが掴みやすいでしょう。

encodeURI と encodeURIComponent の違い

encodeURI() はURL全体を対象とし、予約文字18文字と # をエンコードしません。一方 encodeURIComponent() はクエリの値など「部品」を対象とし、/ ? # & = : なども含めてエンコードします。クエリパラメータの値には後者を使うのが鉄則です。

JavaScript URLエンコード関数の比較
関数主な用途スペース出力備考
escape()(使用禁止)%20非推奨。Latin-1依存でUTF-8非対応
encodeURI()URL全体%20予約文字と # は残す
encodeURIComponent()クエリ値・パス部品%20! ' ( ) * ~ は残す点に注意
URLSearchParamsクエリ文字列全体+form-urlencoded準拠

escape() は非推奨(deprecated)です。%uXXXX という非標準形式を使い、マルチバイト文字を正しく扱えないため、必ず encodeURIComponent() に置き換えてください。

// クエリ値は encodeURIComponent で安全に組み立てる
const q = "C++ & Rust";
const url = `/search?q=${encodeURIComponent(q)}`;
// → /search?q=C%2B%2B%20%26%20Rust

// 複数パラメータは URLSearchParams が確実
const p = new URLSearchParams();
p.append("q", q);
p.append("page", "1");
// → q=C%2B%2B+%26+Rust&page=1  (スペースは + になる)

+ と %20 ― form-urlencoded の罠

HTMLフォーム由来の application/x-www-form-urlencoded では、スペースが %20 ではなく + にエンコードされます。URLSearchParams もこの方式に従うため、encodeURIComponent() の出力(%20)と混在すると不一致が起きます。

RFC 3986 と form-urlencoded の差異
項目RFC 3986form-urlencoded
スペース%20+
リテラルの +そのまま%2B にする必要
JSの実装encodeURIComponent()URLSearchParams
PHPの実装rawurlencode()urlencode()

二重エンコードと2026年のセキュリティ動向

二重エンコード(double encoding)は % 自体を %25 にさらにエンコードしてしまう現象です。hello world を2回エンコードすると hello%2520world となり、1回デコードしても hello%20world までしか戻りません。

これは単なるバグに留まらず、OWASPが警告するセキュリティ問題でもあります。2025年にはAstroで二重URLエンコードを使った認証バイパス(CVE-2025-66202)が報告されており、フレームワーク層でのデコード多重化は今も実際の脆弱性を生んでいます。

実装面では、孤立サロゲートを encodeURIComponent() に渡すと URIError が出るため、ES2024で標準化された String.prototype.toWellFormed() で事前に整える手法が定着しつつあります。デコード側も try/catch でフォールバックを用意しておきましょう。