URLエンコーダー / デコーダー

教科書どおりのパーセントエンコーディング — RFC 3986 の予約文字と非予約文字

URI の標準である RFC 3986 は、文字を意味のある 2 つの集合にきっちり分けます。非予約(unreserved)集合 — 英字、数字、そして 4 つの記号 - . _ ~ — はエンコードなしで URL のどこにでも置けます。これらをエンコードするのは合法ですが無意味です。%41 と A は同じものを識別し、URL 正規化器はこれらをデコードして戻すことが期待されています。設計の核心は予約(reserved)集合にあります。gen-delims の : / ? # [ ] @ は URL コンポーネント間の境界を示し、sub-delims の ! $ & ' ( ) * + , ; = はコンポーネント内部で意味を運びます。それ以外のすべて — スペース、引用符、山括弧、すべての非 ASCII 文字 — は生のままでは現れることができず、%HH のバイト値としてパーセントエンコードしなければなりません。 決定的な機微は、予約文字においてはエンコード形と生の形が等価ではないことです。生の / はパスのセグメントを区切りますが、エンコードされた %2F はスラッシュに見えるだけのデータです。つまり文字が「エンコードを要するか」は、どのコンポーネントの中にあるかに完全に依存します。/ はパスの中では問題ありませんが、URL を含むクエリパラメータ値の中では %2F にならねばならず、? は最初に現れたときは構造で、それ以降はただのデータです。 URL 構築の実務ルール第 1 条が存在する理由もここにあります。組み立て済みの URL 全体をエンコードしてはいけません。正しくやる方法がないのです — 1 回のパスではデータ文字を取りこぼすか、構造文字を破壊するかのどちらかです。動的な値をそれぞれ個別にエンコードしてから、組み立てましょう。

encodeURIComponent・encodeURI・URLSearchParams

JavaScript には 3 世代のエンコード API が同梱されており、選び間違いはフロントエンドコードで最もよくある URL バグです。encodeURI は入力を完成した URL として扱います。決して現れてはならない文字(スペース、引用符、非 ASCII)はエスケープしますが、予約された区切り文字はすべて保持します — : / ? # & = は手つかずで通過します。だから値のエンコードには役に立ちません。アンパサンド入りの検索語を渡すとアンパサンドが生き残り、パラメータが 2 つに割れます。encodeURIComponent が値のエンコーダです。非予約文字と ! ' ( ) * 以外のすべてをエスケープするので、& も = も / も ? も安全になります。経験則は単純で、ほぼ常に欲しいのは encodeURIComponent であり、URL に連結する前に各値へ適用することです。 どちらの関数も完璧な RFC 3986 準拠ではありません。encodeURIComponent は RFC が sub-delims に分類する ! ' ( ) * を生のまま残します。OAuth 1.0 の署名などバイト単位で厳密なプロトコルでは、この 5 文字を追加の replace で補正する必要があります。太古の escape() 関数は絶対に使ってはいけません — UTF-8 普及以前の遺物で、非ラテン文字を壊します。そして decodeURIComponent は不正なシーケンス(単独の %、%ZZ)に URIError を投げるので、信頼できない入力のデコードは try/catch の中に置くべきです。 現代的な答えは手動エンコードを丸ごと省くことです。URL と URLSearchParams クラスは URL をコンポーネント単位でパース・構築・シリアライズし、各部分を正しいルールで自動的にエンコードします。
const q = 'fish & chips?';

encodeURI(q)             // 'fish%20&%20chips?'   & and ? survive!
encodeURIComponent(q)    // 'fish%20%26%20chips%3F'  safe as a value

// The failure mode:
'https://x.io/s?q=' + encodeURI(q)
// → ?q=fish%20&%20chips?   server sees TWO params: q and ' chips?'

// The modern way — encoding handled per component:
const u = new URL('https://x.io/s');
u.searchParams.set('q', q);
u.toString()             // 'https://x.io/s?q=fish+%26+chips%3F'

+ 対 %20 — 1 つのスペースに 2 つの標準

スペース文字に 2 つのエンコーディングがあるのは、2 つの異なる仕様がスペースを取り合ったからです。RFC 3986 のパーセントエンコーディングは、スペースはどこでも %20 だと言います。しかし HTML フォームはそのルールより古いのです。1990 年代初頭にフォーム送信用として定義され、今日でも WHATWG の URL 標準に規定されている application/x-www-form-urlencoded 形式は、スペースを + に(リテラルのプラスを %2B に)エンコードします。どちらのエンコーディングも、それぞれの文脈では正しい。フォーム送信や URLSearchParams が生成するクエリ文字列は + を使い、パスや encodeURIComponent でエンコードされたものは %20 を使います。 バグは文脈をまたぐときに生まれます。古典的な犠牲者は、正当にプラス記号を含むデータです。国際形式の電話番号(+8210...)や base64 文字列です。phone=+82101234567 をエンコードせずに送ると、サーバーのフォームデコーダが + をスペースに変え、' 82101234567' になります — 誰かが気づく前にデータベースまで生き延びがちな破損です。プラスは %2B として送らねばなりません。逆方向では、JavaScript の decodeURIComponent はフォームエンコーディングを一切知りません。+ をリテラルのプラスのまま残すので、フォームエンコードされたクエリ文字列をこれでデコードすると、すべてのスペースが静かに失われます。URLSearchParams は + を正しくデコードします。 言語ライブラリはこの分裂を API に刻み込んでいます。PHP には urlencode(フォーム式、+)と rawurlencode(RFC 3986、%20)があり、Python の urllib.parse.quote は %20 を、quote_plus は + を使います。紛らわしい名前の Java の URLEncoder はフォームエンコーダで + を生成します — これでパスを組み立てるのは有名な間違いです。迷ったら %20 の方が安全な出力です。フォームデコーダは両方受け付けますが、パスのパーサは %20 しか受け付けません。
// The international phone number bug
fetch('/api/sms?to=+821012345678')
// server-side form decoding: '+' → ' '
// to = ' 821012345678'        ← corrupted, silently

fetch('/api/sms?to=%2B821012345678')
// to = '+821012345678'        ← correct

// Decoding mismatch:
decodeURIComponent('a+b%20c')            // 'a+b c'  (+ kept)
new URLSearchParams('x=a+b%20c').get('x') // 'a b c'  (+ = space)

二重エンコーディング — %2520 バグとそのセキュリティ上の双子

パーセントエンコーディングは冪等ではありません。2 回かけると違う出力になります。% という文字自体が %25 にエンコードされるからです。スペースを 1 回エンコードすると %20、その結果をもう一度エンコードすると % が %25 になり、%2520 が生まれます。ユーザーがページやメールの件名で "New%20York" を目にしたなら、パイプラインのどこかで値が 2 回エンコードされ、1 回しかデコードされなかったのです。よくある犯人は、encodeURIComponent を手ずから呼んだ値を、フレームワーク・HTTP クライアント・テンプレートエンジンがもう一度エンコードするコード — あるいは各ホップが、すでにエンコード済みの next-URL パラメータを再エンコードするリダイレクトチェーンです。 鏡写しのバグの方が危険です。どこかの層が 1 回より多くデコードすると、%2520 は %20 に、さらに本物のスペースに崩れます — 攻撃者はまさにこれを武器化します。パスの ../ シーケンスを検査するセキュリティフィルタは %252e%252e%252f で回避できます。フィルタは 1 回デコードされた文字列を検査し、無害に見える %2e%2e%2f を見て通過させます。後段のコンポーネントがもう一度デコードすると、ディレクトリトラバーサルが息を吹き返します。プロキシ、アプリサーバー、フレームワークを横断してこの形の実在の CVE があり、だからセキュリティの指針は単刀直入です: 信頼境界で正確に 1 回デコードし、デコード済みの値を検証し、検証後は二度とデコードしないこと。 双方向を防ぐエンジニアリングの規律は所有権です。エンコードは URL がコードを離れる直前の最後の瞬間に、正確に 1 つの層が所有する。デコードはリクエストが入ってくる瞬間に、正確に 1 つの層が所有する。ネストさせた覚えのない URL に %25 + 16 進数 2 桁が見えたら、ほぼ間違いなく二重エンコーディングのバグを見ています — 一段ずつデコードして、平文に達するまで何パス必要かを数えてみてください。
'New York'
→ encode once:  'New%20York'      correct
→ encode twice: 'New%2520York'    bug: % became %25

// Path traversal hidden by double encoding:
'../etc/passwd'
→ once:  '%2E%2E%2Fetc%2Fpasswd'   filter catches %2E%2E%2F
→ twice: '%252E%252E%252Fetc...'   filter sees nothing;
                                   second decode revives ../

// Diagnosis: decode until stable, count the passes
decodeURIComponent('New%2520York') // 'New%20York'  (pass 1)
decodeURIComponent('New%20York')   // 'New York'    (pass 2 → double-encoded)

URL の中の非 ASCII — UTF-8 エスケープ・文字化け・Punycode

パーセントエンコーディングは文字ではなくバイトに作用します。だから非 ASCII 文字をエンコードするには、まずバイトへ直列化しなければなりません — そのエンコーディングの選択こそ、歴史が散らかしていった場所です。RFC 3986 に明記され、ブラウザが実装する WHATWG URL 標準が強制する現代のルールは UTF-8 です。韓国語の音節「한」は 3 バイト ED 95 9C になって %ED%95%9C とエンコードされ、絵文字 1 つが十数個のパーセントエスケープに膨れ上がることもあります。ブラウザはこれをユーザーから隠します — アドレスバーはデコード済みの Unicode を表示しますが、ワイヤ上のリクエストはエスケープを運びます。 文字化けは、両端がバイト直列化について合意していないときに起こります。EUC-KR や Latin-1 のバイトでエンコードされた値を UTF-8 としてデコードすると、ゴミ文字か、そもそもデコードエラーになります。UTF-8 以前の Web では風土病であり、レガシーシステムや、既定の URI 文字セットが ISO-8859-1 だった古い Java サーブレットコンテナでは今でも顔を出します。より微妙な変種が Unicode 正規化です。café は合成済みコードポイント 1 つ(NFC)としても、e + 結合アクセント(NFD)としても直列化でき、2 つの形は異なるバイト列にパーセントエンコードされ、文字列比較で一致しません。エンコード前に NFC へ正規化しましょう。 ホスト名はまったく別のルールブックに従います。ホスト部ではパーセントエンコーディングはそもそも許されていません。国際化ドメイン名は代わりに IDNA を通り、Unicode のラベルを ASCII 互換の Punycode 形式へ変換します — 한글.example は xn--bj0bj06e.example になります。証明書やログのあの xn-- 接頭辞は破損ではなく、Unicode ドメインの DNS 安全な綴りです。そして、そっくり文字のフィッシング(IDN ホモグラフ攻撃)のせいで、ブラウザが疑わしい混合スクリプトのホスト名を Punycode 形式で表示するのもこのためです。
最終更新:

ツールについて

URL エンコーダはスペース・&・=・?・/ や非 ASCII など URL 内で安全でない文字を %xx パーセントエンコーディングに変換します。エンコードしないと URL 構造が壊れます。フレームワークは自動でやってくれますが、文字列結合で URL を組み立てる時は手動で必要です。

使い方

  1. Choose Encode to escape characters, or Decode to recover the original.
  2. Paste the URL or query parameter into the input box.
  3. The output updates as you type — no submit button.
  4. Copy the encoded string into your URL builder, fetch call, or query string.
  5. When in doubt, encode each query parameter individually rather than the whole URL.

主な使用例

  • Building a search URL where the query contains spaces or special characters.
  • Generating a redirect URL that passes another URL as a parameter.
  • Constructing a deep link with non-ASCII text (Korean, Japanese, emoji) in the path.
  • Decoding a webhook payload where parameters are URL-encoded twice.
  • Sanity-checking that a third-party callback URL is properly escaped.
  • Encoding a base64 string before placing it in a URL — the + and / are unsafe.

よくある質問

Q. What is the difference between encodeURI and encodeURIComponent?

A. encodeURI keeps reserved URL characters (: / ? & =) intact — use it for whole URLs. encodeURIComponent escapes them too — use it for individual parameter values.

Q. Why is a space sometimes encoded as + and other times as %20?

A. + is the legacy x-www-form-urlencoded form (still used in query strings), %20 is the standard percent-encoding used in URL paths. Servers normally accept either in query strings.

Q. Do I need to encode non-ASCII characters?

A. Yes, for compatibility. Modern browsers display Unicode in the URL bar, but the underlying request encodes them as UTF-8 percent-escapes.

Q. Why does decoding produce strange characters?

A. Usually a charset mismatch — the URL was encoded with one encoding (e.g., latin-1) but decoded as another (UTF-8). Fix the source if you can.