기본 콘텐츠로 건너뛰기

Base64 Padding Policy in Python 3.15

thumbnail

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

댓글

이 블로그의 인기 게시물

Django에서 트랜잭션 관리하기

Django에서 트랜잭션 관리하기 안녕하세요! 오늘은 Django에서 데이터베이스 트랜잭션을 효과적으로 관리하는 방법에 대해 알아보겠습니다. 1. 트랜잭션의 중요성 트랜잭션은 데이터베이스의 일관성과 무결성을 보장하는 중요한 개념입니다. Django에서는 여러 가지 방법으로 트랜잭션을 관리할 수 있습니다. 1.1 기본 개념 원자성(Atomicity) : 트랜잭션은 모두 실행되거나 모두 실행되지 않아야 합니다. 일관성(Consistency) : 트랜잭션 전후로 데이터베이스의 일관성이 유지되어야 합니다. 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않아야 합니다. 지속성(Durability) : 완료된 트랜잭션의 결과는 영구적으로 저장되어야 합니다. 2. Django의 트랜잭션 관리 2.1 기본 설정 # settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mydatabase', 'USER': 'myuser', 'PASSWORD': 'mypassword', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, # 모든 뷰를 트랜잭션으로 래핑 } } 2.2 데코레이터 사용 from django.db import transaction @transaction.atomic def create_order(user, items): order = Order.objects.create(user=...

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드 안녕하세요! 오늘은 AWS S3와 CloudFront를 사용하여 정적 파일을 효율적으로 서빙하는 방법에 대해 알아보겠습니다. 왜 S3와 CloudFront를 사용할까요? 높은 가용성 : AWS의 글로벌 인프라를 활용 빠른 전송 속도 : CloudFront의 CDN 기능으로 전 세계 사용자에게 빠른 전송 비용 효율성 : 사용한 만큼만 지불 보안 : AWS의 보안 기능 활용 확장성 : 트래픽 증가에 자동 대응 1. S3 버킷 설정 1.1 버킷 생성 및 설정 import boto3 def create_s3_bucket(): s3 = boto3.client('s3') # 버킷 생성 bucket_name = 'your-static-files-bucket' s3.create_bucket( Bucket=bucket_name, CreateBucketConfiguration={ 'LocationConstraint': 'ap-northeast-2' } ) # 버킷 정책 설정 bucket_policy = { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObje...

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁 안녕하세요! 오늘은 AWS RDS를 사용하는 Django 애플리케이션의 성능을 최적화하는 방법에 대해 알아보겠습니다. 1. RDS 인스턴스 최적화 1.1 인스턴스 타입 선택 # RDS 인스턴스 크기 조정 import boto3 def resize_rds_instance(): rds = boto3.client('rds') response = rds.modify_db_instance( DBInstanceIdentifier='your-db', DBInstanceClass='db.t3.large', # 워크로드에 맞는 인스턴스 타입 선택 ApplyImmediately=True ) return response['DBInstance'] 1.2 파라미터 그룹 설정 def create_parameter_group(): rds = boto3.client('rds') # PostgreSQL 파라미터 그룹 생성 response = rds.create_db_parameter_group( DBParameterGroupName='django-optimized', DBParameterGroupFamily='postgres13', Description='Optimized parameters for Django applications' ) # 성능 관련 파라미터 설정 parameters = [ { 'ParameterName': 'shared_buffers', 'ParameterValue': '2GB...