[RFC] 8분 읽기

[RFC] Base64 URL 안전 vs 표준 Base64

표준 Base64와 URL 안전 Base64는 거의 똑같아 보이지만 서로의 디코더를 망가뜨립니다. 무엇이 정확히 다른지, 왜 다른지, 그리고 JWT, OAuth, URL에는 어느 변형이 필요한지를 살펴봅니다.

2026년 4월 | standards

// 요약

두 종류의 Base64 알파벳이 존재하며, 둘 다 RFC 4648에 명세되어 있습니다.

표준 Base64(§4) — 문자 A–Z a–z 0–9 + /, = 패딩 사용. MIME, PEM 파일, HTTP Basic Auth, 이메일에 안전. URL, 쿠키, 파일 이름, DNS 라벨에 넣으면 망가집니다.

URL 안전 Base64, 때로는 base64url(§5)로 표기 — 문자 A–Z a–z 0–9 - _, 패딩은 보통 생략. URL, JWT 파트, OAuth state 토큰, 쿠키, DNS, 파일 이름에 안전합니다.

한쪽 알파벳용 디코더는 다른 쪽 출력을 거부합니다. 여러분의 전송 경로에 맞는 변형을 골라서 일관되게 사용하세요.

// 두 알파벳을 나란히 놓고 보기

// 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 폼으로부터 상속), /는 경로 구분자이며, =는 예약된 서브 구분자입니다. 이 세 가지가 모두 들어 있는 표준 Base64 문자열을 URL에 그대로 넣으면, 서로 다른 파서들이 그 의미를 두고 의견이 엇갈립니다. 퍼센트 인코딩이 기술적으로 문제를 해결하긴 합니다(+%2B, /%2F, =%3D). 하지만 결과가 비대하고 보기 흉합니다.

RFC 4648 §5는 URL에서 특별한 의미가 없는 두 개의 ASCII 문자, 즉 -(하이픈)와 _(언더스코어)를 선택함으로써 이 문제를 해결합니다. 둘 다 RFC 3986에 따라 예약되지 않은 문자이며, 모든 URL 파서를 그대로 통과합니다. 패딩 =는 일부 URL 문맥(쿼리 문자열의 키/값 구분자)에서도 문제가 되기 때문에, base64url은 관례적으로 이를 생략합니다.

// 언제 어느 것을 사용할까

  • > 표준 Base64 — 사용 용도:
  • > • MIME 이메일 첨부(RFC 2045)
  • > • PEM 형식 인증서와 키(RFC 7468)
  • > • HTTP Basic Auth 헤더(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 statenonce 토큰
  • > • 매직 링크, 초대 코드, 짧은 URL 토큰
  • > • URL에 복사되어도 살아남아야 하는 쿠키 값
  • > • 해시로부터 생성한 파일 이름(파일 이름에 /는 피하세요!)
  • > • DNS 라벨 및 TXT 레코드(하이픈은 허용, 슬래시는 불가)
  • > • 콘텐츠 주소 지정 스토리지 키(IPFS 계열 시스템)

// 두 변형 사이의 변환

두 알파벳은 위치 62, 63, 그리고 패딩에서만 다르기 때문에, 변환은 세 문자에 대한 찾아 바꾸기에 불과합니다. 여러분이 사용할 법한 모든 런타임에서의 예시입니다.

// 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 코드 챌린지(RFC 7636)도 패딩을 금지합니다.

디코더는 두 가지 모두를 처리해야 합니다. 관대한 디코더는 끝의 =를 모두 제거한 뒤 페이로드 길이가 4의 배수가 아니면 내부적으로 패딩합니다. 엄격한 디코더는 이미 4의 배수로 정렬되지 않은 것은 무엇이든 거부합니다. 인코더를 작성한다면 수신 측의 기대에 맞추고, 디코더를 작성한다면 관대하게 하세요.

패딩이 없는 base64url을 디코딩할 때 얼마나 패딩을 더해야 할까요? 규칙은 다음과 같습니다. 입력 길이 mod 4가 2면 = 두 개를 더하고, 3이면 = 하나를 더하며, 0이면 아무것도 더하지 않고, 1이면 입력이 잘못된 것입니다(어떤 유효한 base64도 그 길이 mod 4를 가지지 않습니다).

// 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에 넣었는데, 미들웨어가 +=만 퍼센트 인코딩한다면, 이제 혼합 인코딩의 페이로드가 됩니다. 일부 디코더에는 유효하고 다른 것들에는 쓰레기입니다.
  • > 쿠키 거부 — 일부 HTTP 서버는 값에 / 또는 =가 있는 쿠키를 스펙상 허용됨에도 거부합니다. URL 안전 Base64는 이 문제를 완전히 피해갑니다.
  • > Windows에서 파일 이름 실패 — Base64 표준 출력에는 경로 구분자인 /가 포함됩니다. 파일 이름을 표준 Base64의 SHA-256으로 붙이면 Windows와 POSIX 모두에서 실패합니다.
  • > 복사-붙여넣기 손상 — 일부 터미널과 채팅 앱은 /에서 워드 랩을 적용하여 표준 Base64 문자열을 보이지 않게 줄 바꿈으로 나눕니다. URL 안전 변형은 이런 일을 일으키지 않습니다.

// 실제 사례 연구: JWT

JSON 웹 토큰은 header.payload.signature 형태로, 점으로 구분된 세 개의 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를 고수하세요. 데이터 URI에서 URL 안전이 중요해지는 유일한 경우는 URI 자체가 다른 URL의 쿼리 파라미터로 삽입될 때인데, 그 시점에는 차라리 전체를 퍼센트 인코딩해야 합니다.

// BASE64URL은 URL 인코딩이 아니다

이름이 헷갈릴 정도로 비슷하기 때문에 명시적으로 말할 가치가 있습니다. URL 인코딩(퍼센트 인코딩이라고도 하며, RFC 3986)은 예약된 문자를 %XX 시퀀스로 변환합니다. 브라우저 주소창에 공백을 입력할 때 일어나는 일이 바로 그것입니다(%20).

URL 안전 Base64는 URL 인코딩이 필요 없도록 설계된 Base64의 한 종류입니다. 이미 URL에 안전한 출력을 생성하므로 퍼센트 인코딩이 필요 없습니다. 절대로 둘을 연쇄적으로 적용하지 않으며, 데이터에 따라 둘 중 하나만 적용합니다.

경험칙: 데이터가 이진(이미지, 해시, 서명)이고 URL에 넣어야 한다면 base64url을 사용하세요. 데이터가 이미 텍스트(쿼리 파라미터 값, 폼 필드)라면 퍼센트 인코딩을 사용하세요.

// 의사 결정 트리

  • > Base64 문자열이 URL, 쿠키 값, 파일 이름, DNS 라벨, JWT, OAuth 플로우에 들어가는가? → URL 안전 Base64.
  • > 이메일 본문, PEM 파일, HTTP Basic Auth 헤더, 데이터 URI, MIME 멀티파트 본문에 들어가는가? → 표준 Base64.
  • > 소비자가 여러분이 통제할 수 없는 라이브러리(JWT 파서, OAuth 공급자, S/MIME 클라이언트)인가? → 그들이 기대하는 것에 맞추세요. 스펙이나 라이브러리 문서를 읽으세요.
  • > 확신이 없다면? → 패딩 없는 URL 안전을 사용하세요. 가장 이식성이 좋은 변형이며, 엄격한 수신 측을 위해 언제든 패딩을 다시 붙일 수 있습니다.

// 직접 해 보기

저희 Base64 인코더는 기본적으로 표준 Base64를 출력하며 URL 안전 출력으로 전환하는 토글이 있습니다. 저희 base64url 인코더는 기본적으로 패딩이 없는 URL 안전을 사용합니다. 두 도구를 나란히 사용해서 정확히 어느 문자가 바뀌는지 확인하세요.

더 읽어 보기:
Base64란 무엇이며 어떻게 작동하는가? — 두 변형의 기반이 되는 6비트 그룹화.
JavaScript에서의 Base64(브라우저 + Node) — 런타임별 헬퍼.
Base64 vs Base64URL vs Base32 — Base64 변형 중 어느 것도 올바른 선택이 아닐 때.