TT Lab
블로그 시작하기
배우기 러닝패스 코스

인가와 입력 처리를 코드로 지키기

본문을 붓지 않고, 모양을 확인하고 받는다

TT Lab 에서 이어서 보기

한 줄 요약

요청 본문을 객체에 통째로 붓지 말고, 받을 필드를 코드에 적은 목록(허용 목록)으로만 받습니다. 그리고 받은 값의 모양(종류·길이·형식)을 스키마로 확인한 뒤에야 저장합니다. 둘 다 어기면 422 로 거절하고 아무것도 바꾸지 않습니다.

왜 이 요구가 필요한가

프로필 수정 API 가 user.update(body) 처럼 본문을 그대로 붓는다고 해 봅시다. 화면에는 이름과 소개만 있지만, 사용자 사전에는 화면에 없는 role 과 credit 도 들어 있습니다. 본문에 {"role": "admin"} 을 넣으면 그 값도 함께 들어갑니다. 이 결함을 Rails 와 Node 쪽에서는 mass assignment, Spring 과 ASP.NET 쪽에서는 autobinding 이라고 부릅니다. 이름은 달라도 원인은 같습니다. 입력을 내부 객체에 바로 묶었기 때문입니다.

금지 목록으로 막으려 하면 실패합니다. 오늘 role 을 막아도 내일 누군가 is_verified 를 더하면 그 필드는 그대로 열립니다. 반대로 허용 목록은 새 필드가 생겨도 기본이 닫혀 있습니다. 조용히 버리는 것도 답이 아닙니다. 모르는 필드를 무시하면 보낸 쪽은 무엇이 무시됐는지 모르고, 오타 난 필드 이름(dispaly_name)도 성공처럼 보입니다.

값의 모양도 마찬가지입니다. 이름이 공백뿐이거나, 줄바꿈 같은 제어 문자가 섞였거나, 동의 여부에 "yes" 가 들어오면 그 값은 화면, 로그, 다른 시스템으로 퍼집니다. 저장한 뒤에 고치는 것보다 들어오는 문에서 막는 편이 훨씬 쌉니다.

어떻게 지키나

pydantic 모델이 허용 목록과 스키마를 한자리에 적게 해 줍니다.

class ProfilePatch(BaseModel):
    model_config = ConfigDict(extra="forbid")       # 모르는 필드는 422
    display_name: str | None = None
    bio: str | None = Field(default=None, max_length=160)
    marketing_opt_in: StrictBool | None = None      # "yes"·1 을 참으로 바꿔 주지 않는다

    @field_validator("display_name")
    @classmethod
    def _name(cls, value):
        return clean_display_name(value)            # 공백을 뺀 뒤에 길이를 잰다

몇 가지를 짚어 둡니다. extra="forbid" 가 허용 목록입니다. 기본값인 ignore 는 조용히 버리는 쪽입니다. 그냥 bool 은 "yes" 나 1 을 참으로 바꿔 받으므로 엄격한 StrictBool 을 씁니다. 길이는 공백을 뺀 뒤에 잽니다. 순서가 바뀌면 공백만 있는 이름이 통과합니다. 그리고 검증이 끝난 뒤에 한꺼번에 반영해야 필드 하나만 틀린 본문이 절반만 저장되는 일이 없습니다.

시험은 응답 코드만 보지 말고 요청 뒤의 사용자 상태도 함께 봅니다. create_app(users) 에 사전을 넘겨 두면, 422 가 돌아왔는데도 이름이 바뀌어 있는 '절반만 저장' 을 바로 붙잡을 수 있습니다. 거절 시험과 함께 정상 값이 통과하는 시험을 하나씩 두면 지나치게 막은 수정도 걸러집니다.

422 응답에는 어느 필드가 틀렸는지만 알려 주고, 보낸 값을 되돌려 보내지 않습니다. FastAPI 의 기본 검증 오류는 입력값을 함께 돌려주는데, 사람들이 실수로 비밀번호를 소개란에 붙여 넣는 일은 생각보다 흔합니다.

현장에서 만나는 모습

가입 API 와 프로필 수정 API 를 서로 다른 사람이 만들면, 한쪽에만 허용 목록이 들어가는 일이 잦습니다. ORM 모델을 그대로 요청 모델로 쓰는 코드도 같은 결함을 만듭니다. DB 열이 하나 늘면 그 열이 곧바로 API 의 입력이 되기 때문입니다. 요청 모델과 저장 모델을 따로 두는 것이 이 모듈의 결론이고, OWASP 의 Mass Assignment Cheat Sheet 도 같은 구조(DTO)를 권합니다.

감사 로그에는 거절한 필드의 이름만 남깁니다. 값까지 남기면 로그가 개인정보 저장소가 됩니다. 'FastAPI 로 서비스 만들기' 코스는 pydantic 으로 입력을 받는 기본을 다뤘고, 이 모듈은 그 입력을 보안 요구사항으로 고정하는 데 집중합니다.

English corner

다음 실습에서 할 것

프로필 수정과 가입이 본문을 사용자 사전에 그대로 붓습니다. 요구 A(정한 필드만, 모르는 필드는 422)와 요구 B(이름·소개·동의·이메일의 모양)를 시험으로 쓰고, pydantic 모델로 고치고, 거절한 필드 이름을 감사 로그에 남깁니다.