[RFC] Base64 URLセーフ vs 標準Base64
標準Base64とURLセーフBase64はほぼ同じに見えますが、互いのデコーダーを壊してしまいます。何が変わるのか、なぜか、JWT・OAuth・URLにはどちらが必要かを正確に解説します。
// TL;DR
Base64のアルファベットは2種類存在し、どちらもRFC 4648で規定されています。
• 標準Base64(§4)— 文字は A–Z a–z 0–9 + / と、= パディング。MIME、PEMファイル、HTTP Basic認証、メールで安全です。URL、Cookie、ファイル名、DNSラベルに入れると壊れます。
• URLセーフBase64、base64url と表記されることもある(§5)— 文字は A–Z a–z 0–9 - _、パディングは通常省略されます。URL、JWTのパーツ、OAuthのstateトークン、Cookie、DNS、ファイル名で安全です。
片方のアルファベットのデコーダーは、もう片方の出力を拒否します。転送経路に合うバリアントを選び、一貫して使ってください。
// 2つのアルファベットを並べて比較
// Position 62 and 63 are the only differences
//
// Standard (RFC 4648 §4) URL-safe (RFC 4648 §5)
// 0–25 A-Z A-Z
// 26–51 a-z a-z
// 52–61 0-9 0-9
// 62 + - (hyphen)
// 63 / _ (underscore)
// pad = = or omitted
// Worked example: encoding 0xFB 0xFF 0xBF
// binary: 11111011 11111111 10111111
// 6-bit: 111110 111111 111110 111111
// values: 62 63 62 63
// Standard: + / + / → '+/+/'
// URL-safe: - _ - _ → '-_-_'
// なぜURLセーフなバリアントが必要なのか
標準アルファベットは、寛容なMIME本文用に設計されました。URLはそうではありません。RFC 3986では + はときに空白にデコードされ(HTMLフォームから継承された挙動)、/ はパス区切り文字、= は予約されたサブデリミタです。これら3つすべてを含む標準Base64文字列をURLに入れると、異なるパーサーがその意味について食い違うことになります。パーセントエンコーディングが技術的には修正してくれますが(+ → %2B、/ → %2F、= → %3D)、結果は膨張して見苦しくなります。
RFC 4648 §5はこの問題を、URLで特別な意味を持たないASCII文字2つ、-(ハイフン)と _(アンダースコア)を選ぶことで解決します。どちらもRFC 3986でunreservedであり、あらゆるURLパーサーを無傷で通過します。パディング = もURLの文脈によっては問題となるため(クエリ文字列ではキー/値の区切り文字)、base64urlでは慣例的に削除されます。
// いつどちらを使うか
- > 標準Base64 — 次の用途で使用:
- > • MIMEメール添付(RFC 2045)
- > • PEM形式の証明書と鍵(RFC 7468)
-
>
• HTTP Basic認証ヘッダー(
Authorization: Basic ...) -
>
• data: URI(
data:image/png;base64,...)—+/=文字はすべて引用属性内で合法 - > • XML-DSIGおよびXML-ENC署名
- > • S/MIME
- > URLセーフBase64 — 次の用途で使用:
- > • JWTのヘッダー、ペイロード、署名(RFC 7515がbase64urlを義務付け)
- > • OAuth 2.0 code_challenge / PKCE(RFC 7636)
-
>
• OpenID Connectの
stateとnonceトークン - > • マジックリンク、招待コード、短縮URLトークン
- > • URLにコピーされても生き残る必要があるCookie値
-
>
• ハッシュから生成されるファイル名(ファイル名に
/は避ける!) - > • DNSラベルおよびTXTレコード(ハイフンは許可、スラッシュは不可)
- > • コンテンツアドレス可能ストレージキー(IPFSのようなシステム)
// 両者間の変換
2つのアルファベットは位置62、63、およびパディングだけが異なるため、変換は3文字の置換だけで済みます。以下、よく使うランタイムでの例です。
// JavaScript (browser + Node)
const toUrlSafe = (b64) =>
b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const fromUrlSafe = (b64u) => {
let s = b64u.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '='; // re-add padding
return s;
};
// Node 16+ native
const url = Buffer.from(data).toString('base64url');
const std = Buffer.from(url, 'base64url').toString('base64');
// Python
import base64
url = base64.urlsafe_b64encode(data).rstrip(b'=') # no padding
std = base64.b64encode(data)
// Go
import "encoding/base64"
url := base64.RawURLEncoding.EncodeToString(data) // no padding
std := base64.StdEncoding.EncodeToString(data)
// PHP
$url = rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
$std = base64_encode($data);
// Java
String url = Base64.getUrlEncoder().withoutPadding().encodeToString(data);
String std = Base64.getEncoder().encodeToString(data);
// Shell (GNU coreutils 9+)
echo -n 'hello' | base64 --wrap=0 | tr '+/' '-_' | tr -d '='
// パディングの問題
標準Base64は常に出力を4文字の倍数まで = でパディングします。URLセーフBase64は 通常 パディングを省略します(RFC 4648 §5はこれを明示的にオプションと呼んでいます)。JWT(RFC 7515)はパディングを完全に禁止しています — eyJhbGciOiJIUzI1NiJ9 には末尾の = がありません。OAuthのcode challenge(RFC 7636)もパディングを禁止しています。
デコーダーは両方に対応する必要があります。寛容なデコーダーは末尾の = を削除し、ペイロード長が4の倍数でない場合は内部でパディングします。厳格なデコーダーは、すでに4で割り切れないものは拒否します。エンコーダーを書くなら、受信側の期待に合わせましょう。デコーダーを書くなら、寛容にしましょう。
パディングなしのbase64urlをデコードするとき、パディングはどれだけ追加すればよいのでしょうか?ルールは次の通りです:入力長を4で割った余りが2なら == を2つ、3なら = を1つ、0なら何も追加しません。1なら入力は無効です(mod 4が1になる有効なbase64は存在しません)。
// Restore padding before decoding a JWT segment
function addPadding(b64u) {
const mod = b64u.length % 4;
if (mod === 2) return b64u + '==';
if (mod === 3) return b64u + '=';
if (mod === 0) return b64u;
throw new Error('Invalid base64url length');
}
// 混ぜると何が壊れるか
-
>
サイレントなデータ破損 — 標準デコーダーにURLセーフなペイロードを食わせると、ほとんどの文字は正しくデコードされますが、
-と_はそのアルファベットに ありません。実装によっては例外を投げたり、無視したり、静かに誤ったバイトを生成したりします。 - > 長さのミスマッチ — デコーダーは長さが4の倍数でない文字列を受け取り(パディングが削除されたため)、そのまま拒否します。パディング付き入力を期待するJWTライブラリでの頻繁な悩みの種です。
-
>
パーセントエンコーディングの驚き — 誤って標準Base64をURLに入れ、ミドルウェアが
+と=だけをパーセントエンコードすると、混在したエンコーディングのペイロードができあがります:一部のデコーダーには有効で、別のデコーダーにはゴミです。 -
>
Cookieの拒否 — 一部のHTTPサーバーは、仕様上は許可されていても、値に
/や=を含むCookieを拒否します。URLセーフBase64を使えばこれを完全に回避できます。 -
>
Windowsでのファイル名失敗 — Base64標準の出力には
/が含まれ、これはパス区切り文字です。SHA-256の標準Base64でファイルを命名すると、WindowsでもPOSIXでも失敗します。 -
>
コピペでの破損 — 一部のターミナルやチャットアプリは
/で単語を折り返し、標準Base64文字列を目に見えない形で改行してしまいます。URLセーフはこれを引き起こしません。
// 実例:JWT
JSON Web Tokenは header.payload.signature の形で、3つのbase64url文字列をドットで区切ったものです。主要なJWTライブラリ(jose、jsonwebtoken、PyJWT、golang-jwt、java-jwt)はすべてパディング なし のbase64urlでエンコードします。トークンは日常的にURLやHTTPヘッダーに配置されるためです。
ブラウザで atob() を使ってJWTを手動でデコードする場合、まず変換する必要があります:- を + に、_ を / に置換し、欠落している = を追加します。joseのようなライブラリはこれを内部で処理します。
// Decode a JWT payload in the browser
function decodeJwt(token) {
const [, payloadB64u] = token.split('.');
let b64 = payloadB64u.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
const json = atob(b64);
return JSON.parse(json);
}
// Example JWT payload
decodeJwt('eyJhbGciOiJIUzI1NiJ9' +
'.eyJzdWIiOiJhYmMxMjMifQ' +
'.sig').sub; // 'abc123'
// データURI:エッジケース
データURI(data:image/png;base64,...)は技術的にはURLですが、RFC 2397により + と / を含む完全な標準Base64アルファベットを受け入れます。だからこそ、至るところでデータURI内に標準Base64を見かけるのです — それが仕様です。
データURI内のURLセーフBase64は技術的にRFC 2397に違反しており、厳格なパーサーに拒否される可能性があります。データURIには標準Base64を使いましょう。URLセーフが問題になる唯一のケースは、データURI自体が別のURLにクエリパラメータとして埋め込まれる場合です — その時点では全体をパーセントエンコードすべきです。
// BASE64URLはURLエンコーディングではない
名前がややこしいほど似ているので、明示的に述べる価値があります。URLエンコーディング(パーセントエンコーディング、RFC 3986とも呼ばれる)は予約文字を %XX シーケンスに変換します — ブラウザのアドレスバーに空白を入力したときに起こる挙動(%20)です。
URLセーフBase64は、URLエンコーディングを 必要としない ように設計された Base64のフレーバーです。すでにURLセーフな出力を生成するため、パーセントエンコーディングは不要です。両方を連続で適用することはありません。データに応じてどちらか一方を適用します。
目安:データがバイナリ(画像、ハッシュ、署名)でURLに入れる必要があるなら、base64urlを使用。データがすでにテキスト(クエリパラメータ値、フォームフィールド)なら、パーセントエンコーディングを使用。
// 決定木
- > Base64文字列はURL、Cookie値、ファイル名、DNSラベル、JWT、OAuthフローに入りますか? → URLセーフBase64。
- > メール本文、PEMファイル、HTTP Basic認証ヘッダー、データURI、MIMEマルチパート本文に入りますか? → 標準Base64。
- > 消費者が自分の制御外のライブラリ(JWTパーサー、OAuthプロバイダ、S/MIMEクライアント)ですか? → 彼らが期待するものに合わせ、仕様またはライブラリのドキュメントを読みましょう。
- > わからない? → パディングなしのURLセーフを使用。最もポータブルなバリアントで、厳格な受信側のために後からパディングを付け足すこともできます。
// 自分で試してみる
弊社の Base64エンコーダー はデフォルトで標準Base64を出力し、URLセーフ出力へのトグルがあります。base64urlエンコーダー はパディングなしのURLセーフがデフォルトです。両者を並べて使い、どの文字が変わるかを正確に確認してください。
さらに読む:
• Base64とは何か、どのように動作するのか? — 両バリアントの根底にある6ビットグルーピング。
• JavaScript(ブラウザ + Node)でのBase64 — ランタイム別のヘルパー。
• Base64 vs Base64URL vs Base32 — どのBase64バリアントも正解でない場合。