Stateless Tools

존재 이유가 다릅니다

Base64는 바이너리를 텍스트로

이미지나 암호화된 바이트처럼 글자가 아닌 데이터를 텍스트만 다룰 수 있는 통로(메일 본문, JSON 문자열, HTTP 헤더)에 실어 보내기 위한 것입니다. 3바이트를 4글자로 바꾸므로 결과가 약 33% 커집니다.

URL 인코딩은 구분자를 보호

URL에서 ?, &, =, /는 구조를 나타내는 기호입니다. 값 안에 이 기호가 들어가면 구조가 깨지므로 %26 같은 형태로 바꿔 "이건 데이터다"라고 표시합니다. 크기 증가는 바꾼 문자에만 생깁니다.

서로 대체할 수 없습니다

Base64 결과에는 +와 /가 나오는데 이 둘은 URL에서 특별한 의미가 있습니다. 반대로 URL 인코딩은 바이너리를 텍스트로 만들어주지 못합니다. 목적이 겹치지 않으니 상황에 따라 골라야 하고, 때로는 둘 다 필요합니다.

값을 직접 넣어보면서 읽으시려면 Base64 인코더/디코더와 URL 인코더/디코더를 띄워두세요.

가장 흔한 사고: 플러스가 공백이 됩니다

Base64 값을 URL 쿼리에 그대로 붙였을 때 나는 문제입니다. 재현이 들쭉날쭉해서 원인 찾기가 오래 걸립니다.

무슨 일이 벌어지나요?

HTML 폼 전송 방식(application/x-www-form-urlencoded)에서는 +가 공백을 뜻합니다. 그래서 ?token=aGVsbG8+d29ybGQ=를 서버가 파싱하면 +가 공백으로 바뀌어 aGVsbG8 d29ybGQ=가 됩니다. Base64 디코딩은 실패하거나, 더 나쁘게는 엉뚱한 바이트를 내놓습니다.

왜 어떤 값은 되고 어떤 값은 안 되나요?

Base64 결과에 +가 없으면 아무 문제가 없습니다. 입력 데이터에 따라 +가 나올 때만 깨집니다. 그래서 "테스트할 때는 됐는데 운영에서 어떤 사용자만 실패"하는 형태로 나타납니다. /와 =도 각각 경로 구분자와 쿼리 구분자로 오해될 수 있습니다.

URL-safe Base64가 답입니다

표준 Base64의 +를 -로, /를 _로 바꾼 변형입니다(RFC 4648). 패딩 =는 빼는 경우가 많습니다. JWT가 쓰는 방식이 바로 이것입니다. Base64 도구의 URL-safe 옵션을 켜면 이 형태로 나옵니다.

아니면 Base64 위에 URL 인코딩을 한 번 더

표준 Base64를 유지해야 하면 그 결과를 다시 URL 인코딩합니다. +가 %2B가 되어 안전해집니다. 단 받는 쪽에서 URL 디코딩을 먼저 하고 그다음 Base64 디코딩을 해야 하며, 이 순서를 문서에 적어두지 않으면 다음 사람이 반드시 틀립니다.

이중 인코딩과 %25

%25가 보이면 두 번 인코딩됐습니다

% 자체의 URL 인코딩이 %25입니다. 그래서 이미 인코딩된 %20을 한 번 더 인코딩하면 %2520이 됩니다. URL에 %25가 보이면 대개 프레임워크와 직접 작성한 코드가 둘 다 인코딩한 경우입니다.

리디렉션 파라미터에서 자주 납니다

?next=/login?from=home처럼 URL 안에 URL을 담을 때입니다. 안쪽 URL은 반드시 한 번 인코딩되어야 하고, 그 결과를 다시 인코딩하면 안 됩니다. 로그인 후 리디렉션이 깨지는 흔한 원인입니다. URL 도구의 파라미터 표에 넣어보면 어디까지 풀렸는지 바로 보입니다.

디코딩은 한 번만

"안 풀렸으니 한 번 더" 하는 방식은 위험합니다. 사용자 입력에 %2527처럼 의도적으로 이중 인코딩된 값이 들어오면, 두 번 디코딩하는 코드가 필터를 통과한 뒤에 따옴표를 만들어냅니다. 필터링과 디코딩 횟수가 어긋나면 그 자체가 취약점입니다.

한글과 이모지는 왜 그렇게 길어지나

%EC%95%88 처럼 세 덩이가 나옵니다

URL 인코딩은 바이트를 인코딩합니다. 문자를 인코딩하는 게 아닙니다. "안"은 UTF-8로 3바이트(EC 95 88)이므로 %EC%95%88이 됩니다. 한글 한 글자가 9자로 늘어나는 이유입니다. 이모지는 4바이트라 %F0%9F%98%80처럼 12자가 됩니다.

인코딩 전 문자 집합이 다르면 깨집니다

같은 "안"을 EUC-KR로 인코딩하면 2바이트라 %BE%C8이 됩니다. 보내는 쪽이 UTF-8, 받는 쪽이 EUC-KR로 가정하면 글자가 깨집니다. 오래된 시스템과 연동할 때 나는 문제이고, URL 인코딩 자체의 문제가 아니라 그 전 단계의 문제입니다.

Base64도 같은 원리입니다

Base64 역시 바이트를 다룹니다. 문자열을 Base64로 만들려면 먼저 바이트로 바꿔야 하고, 그때 어떤 문자 집합을 쓰는지가 결과를 바꿉니다. 언어별 기본값이 다르므로 시스템 간에 값을 주고받을 때는 "UTF-8로 바꾼 뒤 Base64"까지 명시하는 편이 안전합니다.

encodeURI와 encodeURIComponent

자바스크립트에서 자주 틀리는 부분입니다. encodeURI는 URL 전체를 인코딩한다고 보고 ?, &, /를 남겨둡니다. encodeURIComponent는 값 하나를 인코딩한다고 보고 전부 바꿉니다. 쿼리 파라미터 값에는 항상 후자를 씁니다. 전자를 쓰면 값에 든 &가 파라미터 구분자로 해석됩니다.

정리하면

  1. 바이너리를 텍스트 통로에 실어야 하면 Base64, URL 구조를 지켜야 하면 URL 인코딩입니다.
  2. Base64 값을 URL에 넣을 때는 URL-safe 변형을 쓰거나 한 번 URL 인코딩합니다. 그냥 붙이면 +가 공백이 됩니다.
  3. %25가 보이면 이중 인코딩입니다. 인코딩은 한 번, 디코딩도 한 번입니다.
  4. 둘 다 바이트를 다루므로 그 전에 어떤 문자 집합으로 바꿨는지가 결과를 바꿉니다.
  5. 자바스크립트에서 쿼리 값에는 encodeURIComponent를 씁니다.
  6. 어느 쪽도 암호화가 아닙니다. 누구나 원래 값으로 되돌릴 수 있습니다.

이어서 볼 페이지: Base64 인코더/디코더에서 URL-safe 옵션을 켜보고, URL 인코더/디코더의 파라미터 표로 어디까지 디코딩됐는지 확인하고, JWT가 URL-safe Base64를 쓰는 방식은 JWT 디코더에서 볼 수 있습니다.

자주 만나는 오류

어느 쪽을 써야 할지 헷갈릴 때

텍스트가 아닌 데이터(이미지, 인증서, 바이너리)를 텍스트만 다니는 통로에 실어야 하면 Base64입니다. URL의 경로나 쿼리에 특수문자를 안전히 넣어야 하면 percent encoding입니다. 둘은 대체재가 아닙니다.

Base64 문자열이 URL에서 깨질 때

표준 Base64의 +가 공백으로, /가 경로 구분자로 해석됩니다. URL-safe Base64로 바꾸거나, Base64 결과를 percent encoding으로 한 번 더 감싸세요. 후자를 쓸 때는 디코딩 순서를 반대로 해야 합니다.

Base64가 암호화라고 오해할 때

Base64는 누구나 되돌릴 수 있는 인코딩입니다. 비밀을 감추는 기능이 전혀 없습니다. Basic 인증 헤더가 Base64인 것도 보안이 아니라 전송 호환성 때문입니다.

사용 순서와 입력 예시 더 보기

실제 입력과 결과

같은 문자열을 두 방식으로

입력

hello world?

결과

Base64            aGVsbG8gd29ybGQ/
Percent encoding  hello%20world%3F

Base64는 바이트를 다른 문자 집합으로 옮겨 담는 것이고, percent encoding은 URL에서 특별한 뜻을 가진 문자만 바꿉니다. 목적이 다릅니다.

Base64를 URL에 넣을 때

입력

a+b/c=  (표준 Base64)

결과

a-b_c   (URL-safe Base64)
+ -> -,  / -> _,  = 패딩 제거

표준 Base64의 +와 /는 URL에서 다른 의미로 읽힙니다. JWT가 URL-safe 변형을 쓰는 이유입니다.