![]()
Python 3.15의 base64 ‘패딩 옵션’이 반가운 이유: URL-safe 토큰/서명 검증에서 자주 깨지는 지점 정리
meta_description: CPython PR #147974(gh-73613)는 Python 3.15에서 base64/base32 인코딩·디코딩에 padded 옵션을 추가해, ‘=’ 패딩이 없는 입력도 더 명확하게 처리할 수 있게 했다. 이 글은 왜 패딩이 문제를 만드는지(웹 토큰, URL-safe, 로그/전달 과정의 손실), 어떤 함수가 어떻게 바뀌는지(padded 기본값/strict 모드와의 관계), 그리고 서비스 코드에서 안전하게 마이그레이션하는 체크리스트를 정리한다. meta_keywords: python,base64,binascii,base32,padding,urlsafe,token,encoding,decoding,validation,strict_mode,ignorechars,migration,backward compatibility,security,web,jwt,cookie,api,python 3.15 meta_robots: index,follow
운영하다 보면 base64는 “그냥 인코딩”이 아니다.
- 토큰
- 쿠키
- URL 파라미터
- 서명/검증용 바이트
이런 데서 base64가 슬쩍 끼고, 그 순간부터 장애가 ‘간헐적’으로 보이기 시작한다.
특히 자주 깨지는 패턴이 이거다.
- 어떤 환경에서는 잘 디코딩되는데
- 어떤 환경에서는
Incorrect padding류 에러가 난다
대부분 원인은 같다.
- ’=’ 패딩이 중간 전달 과정에서 잘리거나, 아예 없는 형태로 들어오기 때문
최근 CPython에 들어간 PR 하나가 이 문제를 꽤 깔끔하게 정리했다.
- CPython PR #147974 (gh-73613): Base32/Base64 without padding 지원
핵심은 새로운 옵션 한 단어다.
padded=
이번 글은 3.15 변경을 “기능 소개”가 아니라, 실무 마이그레이션 관점으로 정리한다.
![]()
1) 왜 base64 패딩이 문제를 만들까(웹에서 특히)
base64는 원래 데이터 길이를 4의 배수로 맞추기 위해 =로 패딩을 붙인다.
그런데 웹에서는 이 =가 너무 자주 “문제처럼” 취급된다.
- 쿼리스트링에서
=는 키/값 구분자로 쓰이고 - URL-safe base64는
+//를-/_로 바꾸는 정도로 끝나지 않고 - 로그/리다이렉트/프록시를 거치며 문자열이 조금씩 변형될 수 있다
결국 현실에서 흔히 보는 입력은 두 종류다.
- 패딩이 있는 정석(base64 spec)
- 패딩이 없는 관행(특히 토큰/URL-safe)
문제는 파이썬 코드가 이 둘을 “명시적으로” 구분하기 어렵다는 점이었다.
- 어떤 함수는 느슨하게 받아주고
- 어떤 함수는 엄격하게 에러를 낸다
그래서 장애가 재현이 어려워진다.
2) PR #147974의 핵심 변경: padded 옵션으로 ‘정책’을 코드로 고정
diff를 보면 바뀐 포인트가 명확하다.
base64 모듈
base64.b64encode(..., *, padded=True, wrapcol=0)base64.b64decode(..., *, padded=True)base64.urlsafe_b64encode(..., *, padded=True)base64.urlsafe_b64decode(..., *, padded=False)← 여기 기본값이 포인트
binascii 모듈
binascii.a2b_base64(..., *, padded=True, alphabet=..., strict_mode=...)
즉, 이제는 “패딩을 요구하는지/허용하는지”가 옵션으로 드러난다.
실무적으로 좋은 점은 2가지다.
1) 장애가 나면 원인이 더 명확해진다 2) 팀 내부 규칙을 코드 레벨에서 강제할 수 있다
3) 특히 urlsafe_b64decode 기본값 변화가 실무에 주는 의미
PR의 문서 변경을 보면 urlsafe_b64decode는 다음이 강조된다.
padded파라미터 추가- 입력 패딩이 기본적으로 더 이상 필요하지 않다(기본값
padded=False)
이건 실무에서 꽤 반갑다.
왜냐하면 URL-safe 토큰을 다루는 코드에서 흔히 이렇게 생겼다.
- 패딩 없는 base64url 문자열
그리고 팀들은 임시방편으로 이런 코드를 썼다.
import base64
def add_padding(s: str) -> str:
return s + '=' * (-len(s) % 4)
payload = base64.urlsafe_b64decode(add_padding(token_part))
이건 잘 돌아가긴 하지만,
- “우리가 어떤 입력을 허용하는지”가 암묵적이고
- 실수로 잘못된 문자열도 통과시키기 쉬운 편이다.
3.15에서는 의도를 좀 더 드러낼 수 있다.
import base64
def decode_base64url(s: str) -> bytes:
# URL-safe 토큰은 패딩이 없는 형태도 정상으로 취급
return base64.urlsafe_b64decode(s, padded=False)
여기서 중요한 건 “편해졌다”가 아니라,
- 정책이 코드에 드러난다는 점이다.
4) 마이그레이션 체크리스트: 어디서 padded=True를 강제해야 하나
모든 곳에서 느슨하게 받으면 더 위험해질 수 있다.
그래서 나는 입력을 두 종류로 나눈다.
A) 외부 입력(사용자/클라이언트/서드파티)
- 기본은 엄격하게:
padded=True+ 가능한 경우 strict 모드 - 실패하면 400으로 떨어뜨리고, 원인을 로그로 남긴다
외부 입력을 “대충 디코딩해서 넘어가기” 시작하면,
- 나중에 검증/서명 단계에서 더 큰 사고로 터진다.
B) 내부 입력(우리 서비스가 만든 토큰/키)
- 포맷이 이미 정해져 있으면 그 포맷으로 강제
- base64url 토큰을 쓰기로 했으면:
padded=False를 명확히
여기서 요점은 이거다.
- 느슨함은 정책이어야지, 우연이면 안 된다
5) 테스트를 어떻게 짜면 ‘패딩 장애’가 재발하지 않나
패딩 이슈는 보통 “특정 길이”에서만 터진다.
그래서 테스트는 2가지만 있으면 된다.
1) 패딩이 필요한 길이(예: 바이트 길이가 1, 2 mod 3인 케이스) 2) 패딩이 없는 입력을 넣었을 때 허용/거부가 의도대로 되는지
예시(아이디어만):
b64decode(..., padded=True)에는 패딩 없는 입력이 실패해야 한다urlsafe_b64decode(..., padded=False)에는 패딩 없는 입력이 통과해야 한다
이걸 테스트로 박아두면,
- 프록시/로그/클라이언트 변경으로 토큰이 미묘하게 변해도
- 서비스가 “어떤 입력을 받아야 하는지”가 흔들리지 않는다.
![]()
6) 실무 팁: padded 옵션을 도입할 때 ‘호환성 경계’를 먼저 그려라
이번 변경은 옵션이 늘어난 거라서, 당장 모든 코드가 깨지진 않는다.
그런데 실제로는 이렇게 진행하면 사고가 난다.
- 일부 경로만 3.15로 올라가고
- 일부 경로는 3.14에 남아 있고
- 토큰/쿠키가 서로 오간다
이럴 때는 ‘호환성 경계’를 먼저 그리는 게 정답이다.
(1) 우리가 다루는 문자열이 base64인지, base64url인지부터 명시
- base64:
+/= - base64url:
-_(그리고 패딩은 흔히 생략)
이걸 문서/코드 주석에 박아두면, 나중에 디버깅이 훨씬 빨라진다.
(2) 인터페이스별로 디코딩 정책을 다르게
- 외부 API 입력: 가능한 엄격하게
- 내부 토큰: 포맷을 정했으면 그 포맷으로 강제
그리고 정책을 함수로 감싼다.
import base64
def decode_token_part(s: str) -> bytes:
# 우리 서비스 토큰은 base64url + no padding
return base64.urlsafe_b64decode(s, padded=False)
def decode_external_b64(s: str) -> bytes:
# 외부 입력은 패딩을 요구
return base64.b64decode(s, padded=True)
이렇게 감싸두면, 버전이 올라가도 팀이 ‘어느 경로에서 느슨함을 허용했는지’ 잊지 않는다.
(3) 로그에 남길 건 문자열이 아니라 “길이+타입”
base64 토큰을 통째로 로그에 찍는 건 보안상 위험할 수 있다.
대신 이런 정보를 남기면 재현에 충분하다.
- 입력 길이
- base64 vs base64url 판정
- padded 정책(True/False)
패딩 관련 장애는 대부분 “길이”에서 단서가 나온다.
7) 결론: base64는 기능이 아니라 ‘입력 정책’이다
정리하면,
- base64 패딩은 스펙 관점에서는 정상인데
- 웹 전달 관점에서는 자주 손상된다
Python 3.15의 padded= 옵션은 그래서 좋다.
- 입력 정책을 코드로 고정할 수 있고
- 팀 규칙(엄격/느슨)을 케이스별로 분리할 수 있다
이런 종류의 변화는 성능보다 “운영 안정성”을 올려준다.
Sources
- CPython PR #147974 — gh-73613: Support Base32 and Base64 without padding
- https://github.com/python/cpython/pull/147974
Keywords
python,base64,binascii,base32,padding,urlsafe,token,encoding,decoding,validation,strict_mode,ignorechars,migration,backward compatibility,security,web,jwt,cookie,api,python 3.15
이미지 크레딧/라이선스
- Macro laptop coding (Unsplash).jpg — Marc Mueller / CC0
- https://commons.wikimedia.org/wiki/File:Macro_laptop_coding_(Unsplash).jpg
- http://creativecommons.org/publicdomain/zero/1.0/deed.en
- Early Mornings - Life Off Screen (Unsplash).jpg — Aaron Burden / CC0
- https://commons.wikimedia.org/wiki/File:Early_Mornings_-_Life_Off_Screen_(Unsplash).jpg
- http://creativecommons.org/publicdomain/zero/1.0/deed.en
댓글
댓글 쓰기