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

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

가져올 곳은 정해 두고, 리다이렉트도 다시 확인한다

TT Lab 에서 이어서 보기

목표

링크 미리보기 기능의 URL 허용 목록과 리다이렉트 재확인을 요구사항 시험으로 적고, urlsplit 과 직접 따라가는 리다이렉트로 고칩니다.

왜 중요한가

서버가 대신 가져오는 기능은 사용자에게 서버의 네트워크 위치를 빌려줍니다. 금지 목록은 표기법과 이름 풀이로 우회되고, 허용 목록도 접미어 비교와 리다이렉트로 다시 열립니다. 보낸 요청까지 확인하는 시험으로 그 틈을 고정합니다.

단계

  1. /opt/app/appsec/az-fetch-lab/ 의 파일을 /root/appsec/az-fetch-lab/ 로 복사하고 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는지 봅니다. REQUIREMENTS.md 를 끝까지 읽습니다.
  2. /root/appsec/az-fetch-lab/test_security.py 에 요구 A(https·사용자 정보 없음·허용 목록의 호스트 정확 일치·443 만, 걸리면 요청을 보내지 않음)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 씁니다. 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 합니다.
  3. service.py 를 고쳐 요구 A 를 지킵니다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 함께 돌립니다.
  4. 요구 B(리다이렉트는 한 걸음마다 check_url 로 다시 확인하고 3번까지만)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더합니다. 판정은 2단계와 같습니다.
  5. service.py 를 고쳐 요구 B 도 지킵니다.
  6. test_functional.py 가 그대로 통과하는지 봅니다. 채점기는 숨은 기능 시험도 함께 돌립니다.
  7. REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부릅니다(막은 요청은 egress.denied, 가져온 것은 egress.fetched, 주소 전체는 남기지 않음).
  8. 채점기가 그럴듯한 오답을 하나씩 끼워 여러분의 test_security.py 를 돌립니다. 오답마다 시험이 하나 이상 실패해야 합니다.

참고

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

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

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

요구 A 를 시험으로 쓴다

/root/appsec/az-fetch-lab/test_security.py 에 요구 A(https·사용자 정보 없음·허용 목록의 호스트 정확 일치·443 만, 걸리면 요청을 보내지 않음)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 쓴다. 이 시험은 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 한다.

http 주소, 사용자 정보가 붙은 주소, notdocs.example.com, 다른 포트를 fetch_preview 에 넘겨 NotAllowed 의 reason 을 보세요. 처리기가 받은 요청을 목록에 모아 두면 요청을 보내지 않았는지도 확인할 수 있습니다.

요구 A 를 지키도록 고친다

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

urlsplit 의 scheme·username·password·hostname·port 를 REQUIREMENTS.md 의 순서대로 확인하세요. 호스트는 허용 목록에 정확히 있어야 하고, port 를 읽다가 ValueError 가 나면 port 로 거절합니다.

요구 B 를 시험으로 쓴다

/root/appsec/az-fetch-lab/test_security.py 에 요구 B(리다이렉트는 한 걸음마다 check_url 로 다시 확인하고 3번까지만)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더한다. 02 단계처럼 지금 코드에서 실패하고 올바른 코드에서 통과해야 한다.

허용된 주소가 302 로 허용 밖 주소를 가리키게 처리기를 만들고, reason 이 redirect 이며 다음 주소로 요청이 가지 않았는지 보세요. 리다이렉트 3번은 되고 4번째에서 멈추는지도 시험할 거리입니다.

요구 B 를 지키도록 고친다

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

client.get(url, follow_redirects=False) 로 한 번씩 가져오고, response.is_redirect 이면 urljoin 으로 다음 주소를 만든 뒤 check_url 을 다시 부릅니다. 반복은 처음 요청을 포함해 MAX_REDIRECTS + 1 번까지입니다.

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

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

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

감사 로그를 남긴다

REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부른다(막은 요청은 egress.denied, 가져온 것은 egress.fetched, 주소 전체는 남기지 않음). 남기지 말라고 한 값은 어떤 항목에도 넣지 않는다.

NotAllowed 를 잡는 자리에서 audit('egress.denied', reason=..., host=...) 를, 가져오기를 마치면 egress.fetched(host, status, hops)를 남깁니다. 경로와 쿼리는 넣지 않습니다.

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

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

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