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

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

본문을 붓지 말고 정한 필드만 받는다

TT Lab 에서 이어서 보기

목표

프로필 수정과 가입 API 의 허용 필드와 값의 모양을 요구사항 시험으로 적고, pydantic 모델로 고칩니다.

왜 중요한가

본문을 객체에 통째로 부으면 화면에 없는 필드(역할, 적립금)까지 바뀝니다. 금지 목록은 새 필드가 생기면 뚫리고, 조용히 버리는 방식은 오타를 성공처럼 보이게 합니다. 허용 목록과 스키마를 코드에 적고 시험으로 고정해 둡니다.

단계

  1. /opt/app/appsec/az-profile-lab/ 의 파일을 /root/appsec/az-profile-lab/ 로 복사하고 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는지 봅니다. REQUIREMENTS.md 를 끝까지 읽습니다.
  2. /root/appsec/az-profile-lab/test_security.py 에 요구 A(PATCH /me 와 /signup 은 정한 필드만 받고, 모르는 필드가 섞이면 422 로 아무것도 바꾸지 않는다)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 씁니다. 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 합니다.
  3. service.py 를 고쳐 요구 A 를 지킵니다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 함께 돌립니다.
  4. 요구 B(이름·소개·동의·이메일의 모양을 확인하고, 어기면 422 로 아무것도 바꾸지 않는다)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더합니다. 판정은 2단계와 같습니다.
  5. service.py 를 고쳐 요구 B 도 지킵니다.
  6. test_functional.py 가 그대로 통과하는지 봅니다. 채점기는 숨은 기능 시험도 함께 돌립니다.
  7. REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부릅니다(거절은 input.rejected, 수정은 profile.updated, 값은 남기지 않음).
  8. 채점기가 그럴듯한 오답을 하나씩 끼워 여러분의 test_security.py 를 돌립니다. 오답마다 시험이 하나 이상 실패해야 합니다.

참고

재료를 복사하고 기능 시험을 돌린다

/opt/app/appsec/az-profile-lab/ 의 파일을 모두 /root/appsec/az-profile-lab/ 로 복사하고, 그 디렉터리에서 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는 것을 본다. REQUIREMENTS.md 를 끝까지 읽는다.

cp 로 디렉터리의 파일을 통째로 옮깁니다. 고치기 전에 지금 무엇이 되는지부터 확인해 두면, 나중에 무엇을 깨뜨렸는지 가릴 수 있습니다.

요구 A 를 시험으로 쓴다

/root/appsec/az-profile-lab/test_security.py 에 요구 A(PATCH /me 와 /signup 은 정한 필드만 받고, 모르는 필드가 섞이면 422 로 아무것도 바꾸지 않는다)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 쓴다. 이 시험은 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 한다.

PATCH /me 에 role 이나 credit 을 넣어 보내고, 응답 코드와 그 뒤의 사용자 사전을 함께 확인하세요. 허용 필드와 금지 필드를 섞어 보냈을 때 허용 필드도 바뀌지 않아야 합니다. 가입 경로도 같은 요구를 받습니다.

요구 A 를 지키도록 고친다

/root/appsec/az-profile-lab/service.py 를 고쳐 요구 A 를 지킨다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 고친 코드에 돌린다.

요청 전용 모델에 model_config = ConfigDict(extra="forbid") 를 두고, ValidationError 를 잡아 422 로 바꾸세요. 반영은 검증이 끝난 뒤 model_dump(exclude_unset=True) 로 합니다. 새 사용자의 role 과 credit 은 본문이 아니라 코드가 정합니다.

요구 B 를 시험으로 쓴다

/root/appsec/az-profile-lab/test_security.py 에 요구 B(이름·소개·동의·이메일의 모양을 확인하고, 어기면 422 로 아무것도 바꾸지 않는다)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더한다. 02 단계처럼 지금 코드에서 실패하고 올바른 코드에서 통과해야 한다.

공백만 있는 이름, 31자 이름, 줄바꿈이 섞인 이름, 문자열 "yes" 인 동의 값, 161자 소개, @ 가 없는 이메일을 보내 보세요. 그리고 정상 값은 통과하는지도 함께 봅니다.

요구 B 를 지키도록 고친다

/root/appsec/az-profile-lab/service.py 를 고쳐 요구 B 도 지킨다. 채점기는 숨은 요구 B 시험과 여러분의 test_b_ 시험을 고친 코드에 돌린다.

StrictBool, Field(max_length=...), field_validator 를 씁니다. 이름은 strip 한 다음에 길이를 재고 제어 문자(유니코드 Cc 분류)를 거릅니다.

기존 기능이 그대로인지 본다

고친 service.py 로 python3 -m pytest -q test_functional.py 가 그대로 통과하는지 본다. 채점기는 재료의 원본 기능 시험에 숨은 기능 시험을 더해 돌린다.

보안 수정이 정상 사용자를 막으면 그 수정은 되돌려집니다. 실패한 기능 시험의 이름이 어떤 정상 동작을 막았는지 알려 줍니다. 막을 것만 막았는지 요구사항의 '그대로 된다' 문장과 견주어 보세요.

감사 로그를 남긴다

REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부른다(거절은 input.rejected, 수정은 profile.updated, 값은 남기지 않음). 남기지 말라고 한 값은 어떤 항목에도 넣지 않는다.

검증 오류를 잡는 자리에서 audit('input.rejected', actor=..., action=..., fields=정렬한 필드 이름) 을 부르세요. 거절된 값은 넣지 않습니다. 반영한 뒤에는 profile.updated 를 남깁니다.

그럴듯한 오답을 시험이 붙잡는지 본다

채점기가 기준 코드에 그럴듯한 오답을 하나씩 끼워 /root/appsec/az-profile-lab/test_security.py 를 돌린다. 오답마다 시험이 하나 이상 실패하도록 필요한 시험을 더한다.

채점 메시지가 어떤 오답이 빠져나갔는지 알려 줍니다. 한 경로만 고친 수정이면 다른 경로를, 비교 방법이 틀린 수정이면 그 비교를 확인하는 시험을 더하세요. 요구사항의 문장 하나하나가 시험 하나가 될 수 있습니다.