Base64・URLエンコーディング:なぜ必要で、いつ誤用されるか

2026-04-13公開 7分で読了

要約 (TL;DR)

echo -n "Hi" | base64 はちょうど4文字(SGk=)を返す。入力2バイト(16ビット)が6ビット単位に切られて出力4文字となり、最後の1コマはパディング = で埋まる——ビット算術はここで終わりだ。この単純な規則を「安全のためにとりあえず包んでおこう」に拡張した瞬間、コストがついてくる。入力3バイトごとに出力4文字という比率は正確に約33%膨張で、ある運用トラフィックでは1.2MBのJPEGをBase64で包んだ結果が約1.64MBに記録されたことがあった。エンコーディングは暗号化でも圧縮でもない——あるアルファベットのデータを、それをそのまま通せないチャネルを通すために別のアルファベットで表現するための規則にすぎない。Base64(RFC 4648 §4)はメール本文、JSON文字列フィールド、インライン画像のようなテキスト専用チャネル向けで、Base64URL(同RFC §5)は2文字(+-/_)を入れ替えてURL・ファイル名・JWTに安全に入れられるようにした派生だ。パーセントエンコーディング(RFC 3986)は別の道具で、URL文法上意味を持つASCII文字(?&#、空白、非ASCII)を %XX で包んでデータであることを示す。チャネルに合わせて選ぶ。バイナリが通るならそのまま、テキストチャネルならBase64、URL文脈ならBase64URL、すでにURLの中にあるならパーセントエンコーディング。そしてアルファベットが公開されているので、見えるBase64文字列は誰でも復号できる——機密の手段に使ってはならない

背景・コンセプト

テキスト専用チャネルは、画像を運ばなければならなくなる遥か以前から存在していた。メールは歴史的に7ビットASCIIを前提とし、上位ビットの立ったバイトを破壊することがあった。HTTPヘッダは今でも一部の文字を禁じており、JSON文字列フィールドはUTF-8だが生の制御バイト(0x00–0x1F)を安全に運べない。RFC 4648で標準化されたBase64は、入力3バイト(24ビット)を正確に6ビットずつ切り出して出力文字4個にマッピングしてこの問題を解く。アルファベットは64文字:A-Z(0–25)、a-z(26–51)、0-9(52–61)、+(62)、/(63)、そして長さを4の倍数に整えるパディング =。入力1バイトは XX==(8ビット → 6+2ビット)、2バイトは XXX=(16ビット → 6+6+4ビット)、3バイトは XXXX(24ビット → 6+6+6+6ビット)になり、3バイト境界で割り切れればパディングは出ない。

**URL安全(URL-safe)**派生はアルファベット索引の62と63だけを置き換える——+-/_。レガシーフォーム送信で + が空白として解釈され、/ がパス区切りであるためだ。URL安全文脈では = パディングも省略されることが多い——これがJWT(RFC 7519)で使われる形式で、ヘッダ.ペイロード.署名 の3段構造が Authorization: Bearer ... ヘッダにコンパクトに収まる必要があるからだ。

パーセントエンコーディング(URLエンコーディングとも呼ばれる)は別物だ。RFC 3986はASCIIを2つに分ける。そのまま使える**非予約(unreserved)文字(A-Za-z0-9-._~)と、構造的に意味を持つためデータとして使うときはエンコードが必要な****予約(reserved)**文字(:/?#[]@!$&'()*+,;=)。この集合の外側のバイトは——UTF-8でエンコードされた非ASCIIバイトを含めて——16進で %XX と表記される。JavaScriptの encodeURIComponent は予約文字まで積極的にエンコードし、1コンポーネント内に安全に入れられるよう設計されている。encodeURI はURL全体を扱う前提でもう少し寛容だ。

3つのエンコーディングは出力バイトが似て見えても互換ではない。パーセントエンコードされたクエリパラメータは結局ASCII文字で別のASCII文字を表しているだけなので、これをBase64デコーダに食わせると意味のないバイトが出る。標準Base64文字列をURLパスにそのまま貼ると、中の / のせいでパスが意図せず切れる——同僚はこの1文字に半日溶かしたことがある。運用で出るエンコーディングのバグの半分は、こうした取り違えから生まれる。

比較・データ

項目Base64(標準)Base64URLパーセントエンコーディング
用途バイナリを拒むテキストチャネル(MIME本文、JSON内バイト)同じだがURL・ファイル名・JWTに入る場合URLの1コンポーネント内の予約文字・非ASCII文字
アルファベットA–Z a–z 0–9 + / =A–Z a–z 0–9 - _(パディング任意)非予約 + 残りは %XX
オーバーヘッド約33%(3バイト → 4文字)、MIMEは76文字改行追加約33%、通常パディングなし可変——UTF-8の1バイトが %XX の3文字に
壊れる場面URLに入った +/、厳格デコーダでパディング欠落+/= 基準の標準デコーダに戻すとき二重エンコード(エンコード済みを再エンコード)
一般用途MIME添付、data URI、JSON内インラインバイナリJWT、URL安全ID、短縮リンククエリパラメータ、パスセグメント、フォーム本文

数字は無視できない。1MBの画像をBase64エンコードすると約1.37MBで、MIMEの76文字改行を加えるとさらに2%程度増える。HTTPレスポンスでこの膨張はサーバー帯域とクライアントパーサの両方に効く。パーセントエンコーディングは普通の文字列レベルではそこまで激しくないが、CJKテキストではバイトが大きく増える。日本語1文字はUTF-8で3バイトなので、ASCII 9バイト(%XX × 3)にエンコードされる。「あ」1文字がURL内では %E3%81%82 の9バイトになる。

実践シナリオ

シナリオ1 — メール添付。 PDFはMIMEパート内に Content-Transfer-Encoding: base64 で乗る。メールクライアントがファイルをBase64エンコードし、古典的なMIME規則どおり76文字で改行を入れて送信する。受信側は逆過程で復元する。SMTPの「テキスト前提」がこの領域でBase64を事実上の既定にし、8BITMIMEやBINARYMIMEといった拡張がある今でも、多くのメールサーバーが安全のためにBase64を使う。33%のオーバーヘッドは確実な配送のための代償として受け入れられている。

シナリオ2 — JSON Web Token。 JWT(RFC 7519)は ヘッダ.ペイロード.署名 で、各セグメントはパディングなしのBase64URLでエンコードされる。URL安全アルファベットのおかげでトークンが Authorization ヘッダ・クエリパラメータ・ログ行に再エスケープなしで入る。パディング省略でトークンが短く保たれる。注意点:JWTをデコードした人は誰でもクレームを読める。エンコーディングはセキュリティではなく、HMAC/RSA署名がセキュリティだ。ペイロードに秘密情報を入れてはいけない。

シナリオ3 — 小さな画像のdata URI。 background-image: url("data:image/png;base64,iVBORw0K...") はPNGをCSSに直接インライン化する。極小アイコンに対しては往復コストを節約できるが、Base64の33%オーバーヘッドとブラウザキャッシュ・並列リクエストの喪失を考えると、HTTP往復より割に合うほど小さな資産でしか有利にならない。僕の経験では損益分岐は1–2KB近辺で、それ以上は別ファイルかSVGの方が普通は速い。

シナリオ4 — ユーザーアバターアップロード。 よくあるアンチパターンは FileReader.readAsDataURL でファイルを読んで data:image/png;base64,... 文字列に変換し、JSONフィールドとしてPOSTすること。動くがほぼ正解ではない。ペイロードが33%膨らみ、サーバーはディスクに書く前にBase64デコードを1回多く挟み、クライアントとサーバーの両方で文字列コピーがメモリを占める。あるケースでは5MBの画像が約6.7MBのJSONに膨らんでモバイル回線でタイムアウトを起こした——multipart/form-data に切り替えたら同じファイルが5MBのまま上がった。サードパーティAPIがJSON文字列しか受け付けないなど、転送層が本当にテキストを要求する場合に限ってBase64を検討すべきだ。

よくある誤解

「Base64は暗号化」。 違う。マッピングはRFC 4648で公開されアルファベットは固定で、どんなデコーダも元のバイトを返す。エンコーディングは転送を保護するだけで、内容を保護しない。機微情報なら先に暗号化し、暗号文を転送用にBase64で包む。

「URLエンコーディングは非ASCII文字だけに必要」。 ASCIIの予約文字もデータとして使うときはエンコードが必要だ。& をそのまま置けばサーバーは新しいパラメータと解釈し、# をそのまま置けばその後がフラグメントとして処理される。規則は「文字集合」ではなく構造に基づく。

「HTTPに出すなら全部Base64で安全」。 HTTPはバイナリ本体を喜んで運ぶ——Content-Type: application/octet-stream、チャンク転送、任意のバイト値。Base64はそれができないチャネルのための回り道で、すでにバイトを扱えるチャネルに33%のオーバーヘッドを乗せるのは純粋な税だ。ファイルアップロードは multipart/form-data か生の本体で、JSONフィールド・URLパラメータ・MIME本文のようにチャネルが本当にテキストを要求する場面に限ってBase64で。

「Base64とBase64URLは入れ替えられる」。 違う。+/ を期待する厳格Base64デコーダにURL安全文字列を入れると失敗するかゴミ値が出る。ライブラリは普通両方を提供するので、エンコーダとデコーダを端から端まで揃えること——Node Buffer.from(s, 'base64') は両方のアルファベットを受け付けるが、すべての言語の標準ライブラリがそこまで寛容なわけではない。

チェックリスト

  1. チャネルが7ビット専用か構造化テキストか? メール本文、JSON内バイト、CSSの data: URI → Base64。
  2. 値がURL・ファイル名・JWTに入るか? Base64URL、パディング許容はコンシューマに合わせる。
  3. 値自体がURLコンポーネントか? 必要な部分だけパーセントエンコード。
  4. データが大きいか? チャネルが本当にテキストを要求するか先に確認。生のバイナリ本体で33%節約できる。
  5. デコーダが厳格か寛容か? 標準/URL安全アルファベットを端から端まで揃える。
  6. 機密保持が目的か? 先に暗号化する。エンコーディングだけでは絶対に秘密にならない。

関連ツール

Patrache Studioの Base64エンコーダ・デコーダ は標準・URL安全派生を両方ともローカルで処理するため、入力バイトがブラウザを離れない——トークンや小さな鍵を覗くときに便利だ。扱うペイロードがJWTなら JSONのフォーマット・構文検証・JSON Schemaの違いと実務での使い分け で、復号したクレームをきれいに検査する方法を一緒に見てほしい。Base64URLでエンコードされた値がUUIDから派生した短いIDなら、UUID v1・v4・v7の比較とDB主キー設計 で、元のバイトのソート・インデックス特性がそのまま重要になる理由を確認できる。

参考文献