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

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

링크를 따라간 실제 경로로 판단한다

TT Lab 에서 이어서 보기

목표

파일 보관함의 내려받기 경로 한정과 올리기 이름 규칙을 요구사항 시험으로 적고, pathlib 의 resolve·is_relative_to 와 허용 목록으로 고칩니다.

왜 중요한가

문자열 접두어 비교는 이름이 비슷한 형제 디렉터리를, abspath 정규화는 심볼릭 링크를, os.path.join 은 절대 경로를 놓칩니다. 세 수정 모두 가장 흔한 시험은 통과하므로, 실제 디렉터리와 링크를 만들어 시험으로 고정해 둡니다.

단계

  1. /opt/app/appsec/az-files-lab/ 의 파일을 /root/appsec/az-files-lab/ 로 복사하고 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는지 봅니다. REQUIREMENTS.md 를 끝까지 읽습니다.
  2. /root/appsec/az-files-lab/test_security.py 에 요구 A(내려받기는 링크를 따라간 실제 경로가 기준 디렉터리 안일 때만)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 씁니다. 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 합니다.
  3. service.py 를 고쳐 요구 A 를 지킵니다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 함께 돌립니다.
  4. 요구 B(올리는 이름은 규칙에 통째로 맞아야 하고, 그 자리에 링크가 있으면 거절)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더합니다. 판정은 2단계와 같습니다.
  5. service.py 를 고쳐 요구 B 도 지킵니다.
  6. test_functional.py 가 그대로 통과하는지 봅니다. 채점기는 숨은 기능 시험도 함께 돌립니다.
  7. REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부릅니다(막은 요청은 path.denied, 올리기는 file.uploaded).
  8. 채점기가 그럴듯한 오답을 하나씩 끼워 여러분의 test_security.py 를 돌립니다. 오답마다 시험이 하나 이상 실패해야 합니다.

참고

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

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

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

요구 A 를 시험으로 쓴다

/root/appsec/az-files-lab/test_security.py 에 요구 A(내려받기는 링크를 따라간 실제 경로가 기준 디렉터리 안일 때만)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 쓴다. 이 시험은 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 한다.

tmp_path 에 data(기준), data-old(형제), outside(바깥)를 만들고 data 안에 바깥을 가리키는 링크를 두세요. ../data-old/…, 절대 경로, 링크 이름을 resolve_download 에 넘기면 PermissionError 여야 합니다. 안쪽 파일은 그대로 열려야 합니다.

요구 A 를 지키도록 고친다

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

Path(base).resolve() 와 (root / relpath).resolve() 를 구한 뒤 is_relative_to 로 비교하세요. 밖이면 PermissionError, 안인데 파일이 없으면 FileNotFoundError 입니다.

요구 B 를 시험으로 쓴다

/root/appsec/az-files-lab/test_security.py 에 요구 B(올리는 이름은 규칙에 통째로 맞아야 하고, 그 자리에 링크가 있으면 거절)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더한다. 02 단계처럼 지금 코드에서 실패하고 올바른 코드에서 통과해야 한다.

../x.txt, a/b.txt, .hidden, 65자 이름이 ValueError 인지, uploads 안에 링크를 미리 만들어 둔 이름도 거절되는지 보세요. 정상 이름의 저장 자리가 uploads 바로 아래인지도 확인합니다.

요구 B 를 지키도록 고친다

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

re.fullmatch 로 이름 전체를 규칙에 대 보고, 저장 자리 Path 의 is_symlink() 를 확인하세요. 둘 다 ValueError 로 거절합니다.

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

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

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

감사 로그를 남긴다

REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부른다(막은 요청은 path.denied, 올리기는 file.uploaded). 남기지 말라고 한 값은 어떤 항목에도 넣지 않는다.

PermissionError·ValueError 를 내기 직전에 audit('path.denied', action=..., reason=..., requested=받은 값) 을 부릅니다. 없는 파일은 기록하지 않고, 저장을 마친 save_upload 에서 file.uploaded 를 남깁니다.

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

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

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