メインコンテンツへ移動

URLエンコード・デコード

URLエンコード・デコードは、テキストのパーセントエンコードとデコードをブラウザ内で行います。モードの選択は見た目以上に重要です。クエリ文字列ではスペースは+ですが、パスでは%20であり、間違った方でフォームボディをデコードすると、エラーは一切出さずに「a + b」が「a b」に変わります。

端末内で処理
モード
プレーンテキスト
0 B1 行行 1、列 1
エンコード済み読み取り専用
0 B1 行

URLエンコード・デコードはどう動きますか?

「URLエンコード」という名前の下には、3つの異なるエンコードがあります。間違ったものを選ぶと、決してエラーを投げないバグが生まれます。

パーセントエンコードとフォームエンコードは別物

RFC 3986のパーセントエンコードでは、スペースは%20と書きます。application/x-www-form-urlencoded形式(クエリ文字列とフォームのPOSTボディが実際に使う形式)では、スペースは+と書かれます。つまり、リテラルのプラスは%2Bと書く必要があります。フォームボディをdecodeURIComponentでデコードすると、+はそのまま残ります。その結果、検索語「a + b」は、ユーザーがプラスを入力したときには「a + b」として届き、スペースを入力したときには「a+b」として届きます。どちらか一方は間違いなのに、どちらもエラーを出しません。

encodeURIComponentはRFCが予約する5文字をそのままにする

!、'、(、)、*の5文字はRFC 3986が予約しており、encodeURIComponentはこれらをそのままにします。たいていは無害です。しかし、エンコード結果を入力に署名を計算する場面では無害ではありません。だからこそ、AWS SigV4とOAuth 1.0aのライブラリはどれも独自のエンコーダを同梱しています。strictオプションはこれらもエンコードします。

URLの各構成要素にはそれぞれ固有のルールがある

パスには/を含められます。一方、クエリの値にエンコードしていない&を含めると、そこで2つのパラメータに分断されます。これがコンポーネントモードとURL全体モードの違いです。前者はunreserved以外のすべてをエンコードし、後者はURLの構造を形作る文字を保護します。URL全体をencodeURIComponentでエンコードすると、もはやURLではない文字列ができます。

二重エンコードと、その見分け方

%2520は、エンコーダを2回通ったスペースです。%20の%が、今度は%25としてエンコードされたものです。ログを流し読みする人には「%20」と見え、デコードした値にはリテラルの%20が生じます。このツールは、入力が何回のデコードに耐えるかを数えるので、紛れ込んだ余分な層は、不可思議な挙動ではなく目に見えるものになります。

URLの分解にはプラットフォームのパーサを使う

構成要素の分解には、正規表現ではなくブラウザ自身のURL実装を使っています。URLのパースにはuserinfo、IPv6リテラル、デフォルトポート、パーセントエンコードされたホストといったエッジケースが山ほどあり、プラットフォームに既にあるWHATWGアルゴリズムの方が、どんなパターンよりも短く、より正確だからです。重複するクエリキーは順序を保ったまま残します。多くのAPIがこれに意味を与えているからです。

入力

q=a + b

エンコード結果

component: q%3Da%20%2B%20b
form:      q%3Da+%2B+b

URLエンコード・デコードが対応するオプションとエッジケースは?

技術リファレンスとオプション
パラメータ型デフォルト動作とエッジケース
コンポーネントencodeURIComponentデフォルトクエリ値1つやパスセグメント1つ向けです。A–Z a–z 0–9 - _ . ~と下の5文字以外はすべてエンコードします。
URL全体encodeURI—URL全体向けです。: / ? # [ ] @ ! $ & ' ( ) * + , ; =を残すので、URLがURLのまま保たれます。ここでコンポーネントモードを使うと構造が壊れます。
フォームx-www-form-urlencoded—クエリ文字列やPOSTボディ向けです。スペースは+になり、リテラルの+は%2Bになります。正しくデコードするには、パーセントデコードの前に+を戻す必要があります。さもないと、リテラルの+がスペースに化けます。
base64urlRFC 4648 §5—URL安全なbase64のアルファベットです。+と/の代わりに-と_を使い、パディングは取り除きます。JWTのセグメントがこれを使っています。
RFC 3986厳格真偽値オフencodeURIComponentがそのままにする! ' ( ) *もエンコードします。エンコード結果を署名に使う場合に必要です。
すべてエンコード真偽値オフ予約されないバイトも含め、すべてのバイトをパーセントエンコードします。見栄えは悪いものの有効で、うるさいゲートウェイが受け付ける唯一の形になることもあります。
文字セットUTF-8常にASCII以外はUTF-8のバイト列として、1バイトごとに%XXでエンコードされます。現在のすべてのブラウザとサーバーがこれを想定しています。
不正な入力報告位置付きdecodeURIComponentは素のURIErrorを投げるだけです。こちらでは問題のシーケンスを特定し、名前付きで報告します。

よくある質問

スペースが%20ではなく+になるのはなぜですか?

フォームモードになっているからです。これはクエリ文字列やフォームのPOSTボディでは正しいモードです。この形式は、スペースを+と定義しています。パスセグメントやフラグメントでは、スペースは%20になり、+はリテラルのプラスです。どちらのエンコードも正しいもので、位置に合わない方を使うと値が壊れます。

encodeURIとencodeURIComponent、どちらを使うべきですか?

1つの部品、つまりクエリの値、パスセグメント、フラグメントにはencodeURIComponentを使います。URLとしての機能を保たせたいURL全体にはencodeURIです。判定方法は単純です。エンコードするテキストに構造を担う文字(?、&、/など)が含まれていて、それを機能させたままにしたいならencodeURIが必要で、データに変えてしまいたいならencodeURIComponentが必要です。

%2520とは何ですか?どうやって発生するのですか?

スペースが2回エンコードされたものです。1回目で%20になり、2回目でその%が%25としてエンコードされ、%2520になります。たいていは、アプリケーションコードがエンコードした値を、フレームワークやプロキシがさらにエンコードしたことを意味します。上の深度カウンタは、入力が何回のデコードに耐えるかを示すので、層が1つ多すぎないかを判断できます。

!'()*がエンコードされないのはなぜですか?

RFC 3986が予約しているにもかかわらず、encodeURIComponentは意図的にそのままにします。ほとんどの場合は無害です。ただし、エンコードした文字列を署名の入力に使う場合は話が変わります。署名する側と検証する側が、バイト単位で一致しなければならないからです。だからこそ、AWS SigV4もOAuth 1.0aも独自のエンコーダを仕様として定めています。その状況にあるなら、strictモードを有効にしてください。

base64urlとbase64は同じものですか?

アルゴリズムは同じで、字母が違います。base64が使う+と/は、URLではどちらも意味を持つ文字なので、base64urlはこれらを-と_に置き換え、クエリ文字列で意味を持つ=のパディングも取り除きます。JWTは3つのbase64urlセグメントをドットでつないだものであり、JWTをそのままURLに貼れるのにbase64の塊は貼れないのはこのためです。

入力はアップロードされますか?

いいえ。エンコードとデコードは、ブラウザ内で動く文字列操作です。Networkパネルを開いたまま、入力しながら見てみてください。ここでそれが重要なのは、デコードが必要になるURLには、署名付きのコールバックURL、OAuthのリダイレクト、セッショントークンが頻出するからです。つまり、無害に見える形をした認証情報だということです。