Base64는 바이너리를 텍스트로
이미지나 암호화된 바이트처럼 글자가 아닌 데이터를 텍스트만 다룰 수 있는 통로(메일 본문, JSON 문자열, HTTP 헤더)에 실어 보내기 위한 것입니다. 3바이트를 4글자로 바꾸므로 결과가 약 33% 커집니다.
브라우저 내부 처리
둘 다 "문자를 다른 문자로 바꾼다"는 점만 같습니다. 왜 존재하는지가 달라서, 잘못 고르면 값이 조용히 변형됩니다. 실제로 자주 나는 사고를 중심으로 봅니다.
이미지나 암호화된 바이트처럼 글자가 아닌 데이터를 텍스트만 다룰 수 있는 통로(메일 본문, JSON 문자열, HTTP 헤더)에 실어 보내기 위한 것입니다. 3바이트를 4글자로 바꾸므로 결과가 약 33% 커집니다.
URL에서 ?, &, =, /는 구조를 나타내는 기호입니다. 값 안에 이 기호가 들어가면 구조가 깨지므로 %26 같은 형태로 바꿔 "이건 데이터다"라고 표시합니다. 크기 증가는 바꾼 문자에만 생깁니다.
Base64 결과에는 +와 /가 나오는데 이 둘은 URL에서 특별한 의미가 있습니다. 반대로 URL 인코딩은 바이너리를 텍스트로 만들어주지 못합니다. 목적이 겹치지 않으니 상황에 따라 골라야 하고, 때로는 둘 다 필요합니다.
값을 직접 넣어보면서 읽으시려면 Base64 인코더/디코더와 URL 인코더/디코더를 띄워두세요.
Base64 값을 URL 쿼리에 그대로 붙였을 때 나는 문제입니다. 재현이 들쭉날쭉해서 원인 찾기가 오래 걸립니다.
HTML 폼 전송 방식(application/x-www-form-urlencoded)에서는 +가 공백을 뜻합니다. 그래서 ?token=aGVsbG8+d29ybGQ=를 서버가 파싱하면 +가 공백으로 바뀌어 aGVsbG8 d29ybGQ=가 됩니다. Base64 디코딩은 실패하거나, 더 나쁘게는 엉뚱한 바이트를 내놓습니다.
Base64 결과에 +가 없으면 아무 문제가 없습니다. 입력 데이터에 따라 +가 나올 때만 깨집니다. 그래서 "테스트할 때는 됐는데 운영에서 어떤 사용자만 실패"하는 형태로 나타납니다. /와 =도 각각 경로 구분자와 쿼리 구분자로 오해될 수 있습니다.
표준 Base64의 +를 -로, /를 _로 바꾼 변형입니다(RFC 4648). 패딩 =는 빼는 경우가 많습니다. JWT가 쓰는 방식이 바로 이것입니다. Base64 도구의 URL-safe 옵션을 켜면 이 형태로 나옵니다.
표준 Base64를 유지해야 하면 그 결과를 다시 URL 인코딩합니다. +가 %2B가 되어 안전해집니다. 단 받는 쪽에서 URL 디코딩을 먼저 하고 그다음 Base64 디코딩을 해야 하며, 이 순서를 문서에 적어두지 않으면 다음 사람이 반드시 틀립니다.
% 자체의 URL 인코딩이 %25입니다. 그래서 이미 인코딩된 %20을 한 번 더 인코딩하면 %2520이 됩니다. URL에 %25가 보이면 대개 프레임워크와 직접 작성한 코드가 둘 다 인코딩한 경우입니다.
?next=/login?from=home처럼 URL 안에 URL을 담을 때입니다. 안쪽 URL은 반드시 한 번 인코딩되어야 하고, 그 결과를 다시 인코딩하면 안 됩니다. 로그인 후 리디렉션이 깨지는 흔한 원인입니다. URL 도구의 파라미터 표에 넣어보면 어디까지 풀렸는지 바로 보입니다.
"안 풀렸으니 한 번 더" 하는 방식은 위험합니다. 사용자 입력에 %2527처럼 의도적으로 이중 인코딩된 값이 들어오면, 두 번 디코딩하는 코드가 필터를 통과한 뒤에 따옴표를 만들어냅니다. 필터링과 디코딩 횟수가 어긋나면 그 자체가 취약점입니다.
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로 만들려면 먼저 바이트로 바꿔야 하고, 그때 어떤 문자 집합을 쓰는지가 결과를 바꿉니다. 언어별 기본값이 다르므로 시스템 간에 값을 주고받을 때는 "UTF-8로 바꾼 뒤 Base64"까지 명시하는 편이 안전합니다.
자바스크립트에서 자주 틀리는 부분입니다. encodeURI는 URL 전체를 인코딩한다고 보고 ?, &, /를 남겨둡니다. encodeURIComponent는 값 하나를 인코딩한다고 보고 전부 바꿉니다. 쿼리 파라미터 값에는 항상 후자를 씁니다. 전자를 쓰면 값에 든 &가 파라미터 구분자로 해석됩니다.
+가 공백이 됩니다.%25가 보이면 이중 인코딩입니다. 인코딩은 한 번, 디코딩도 한 번입니다.encodeURIComponent를 씁니다.이어서 볼 페이지: Base64 인코더/디코더에서 URL-safe 옵션을 켜보고, URL 인코더/디코더의 파라미터 표로 어디까지 디코딩됐는지 확인하고, JWT가 URL-safe Base64를 쓰는 방식은 JWT 디코더에서 볼 수 있습니다.
텍스트가 아닌 데이터(이미지, 인증서, 바이너리)를 텍스트만 다니는 통로에 실어야 하면 Base64입니다. URL의 경로나 쿼리에 특수문자를 안전히 넣어야 하면 percent encoding입니다. 둘은 대체재가 아닙니다.
표준 Base64의 +가 공백으로, /가 경로 구분자로 해석됩니다. URL-safe Base64로 바꾸거나, Base64 결과를 percent encoding으로 한 번 더 감싸세요. 후자를 쓸 때는 디코딩 순서를 반대로 해야 합니다.
Base64는 누구나 되돌릴 수 있는 인코딩입니다. 비밀을 감추는 기능이 전혀 없습니다. Basic 인증 헤더가 Base64인 것도 보안이 아니라 전송 호환성 때문입니다.
입력
hello world?
결과
Base64 aGVsbG8gd29ybGQ/
Percent encoding hello%20world%3F
Base64는 바이트를 다른 문자 집합으로 옮겨 담는 것이고, percent encoding은 URL에서 특별한 뜻을 가진 문자만 바꿉니다. 목적이 다릅니다.
입력
a+b/c= (표준 Base64)
결과
a-b_c (URL-safe Base64)
+ -> -, / -> _, = 패딩 제거
표준 Base64의 +와 /는 URL에서 다른 의미로 읽힙니다. JWT가 URL-safe 변형을 쓰는 이유입니다.