[EXT-012][실무] 주소 문자열을 직접 붙이지 않고 안전하게 만들기 > IT 기술 공유

본문 바로가기
사이트 내 전체검색

IT 기술 공유

[EXT-012][실무] 주소 문자열을 직접 붙이지 않고 안전하게 만들기

페이지 정보

profile_image
작성자 기술팀장
댓글 0건 조회 17회 작성일 26-09-08 02:36

본문

[이번 수업]
검색어에 공백이나 `&`가 있으면 손으로 붙인 주소의 뜻이 달라질 수 있습니다. URL(웹 자원의 주소)의 구성 요소와 퍼센트 인코딩을 배우고 쿼리를 안전하게 만듭니다.

[선수지식]
HTTP 요청, 문자열, 딕셔너리를 알고 Python 파일을 실행할 수 있으면 됩니다. 바이트와 UTF-8의 기본 개념도 필요합니다.

[학습목표]
1. URL의 스킴·호스트·경로·쿼리·프래그먼트를 구분한다.
2. 사용자 값을 구성 요소에 맞는 함수로 인코딩한다.
3. 서버가 가져올 URL의 목적지를 파싱 후 검증한다.

[핵심개념]
RFC 3986의 URI 구조에서 `https`는 스킴, `example.invalid`는 호스트, `/search`는 경로, `?q=...`는 쿼리, `#help`는 프래그먼트입니다. 프래그먼트는 문서 안 위치를 가리키며 HTTP 요청 대상에는 포함되지 않습니다. 경로와 쿼리의 규칙이 달라 URL 전체를 한 번에 인코딩하면 안 됩니다.

퍼센트 인코딩은 문자 바이트를 `%HH`로 나타냅니다. 쿼리 폼의 `application/x-www-form-urlencoded` 규칙에서는 공백이 `+`가 되고, 값 안의 `&`는 `%26`이 되어 새 매개변수 구분자로 해석되지 않습니다. 반면 경로 조각은 별도의 경로 규칙과 `quote` 같은 함수를 사용합니다. 같은 값을 두 번 인코딩하면 `%`가 `%25`로 바뀔 수 있습니다.

URL을 읽을 때도 문자열의 앞부분이나 정규식만 보지 말고 표준 파서로 나눈 뒤 스킴, 호스트, 포트를 각각 검사합니다. `urljoin`은 절대 입력을 받으면 호스트도 바꿀 수 있어 신뢰하지 않는 값에 바로 쓰지 않습니다. 파서는 모든 입력을 검증하지 않으므로 애플리케이션 정책이 추가로 필요합니다.

[따라하기]
아래를 url_check.py로 저장합니다. 예약된 `.invalid` 도메인을 사용하므로 실제 서버 요청은 발생하지 않습니다.

```python
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit

params = {"q": "한글 & 공백", "page": 2}
query = urlencode(params)
url = urlunsplit(("https", "example.invalid", "/search", query, ""))
parts = urlsplit(url)
decoded = parse_qs(parts.query, strict_parsing=True)

print("주소:", url)
print("호스트:", parts.hostname)
print("검색어:", decoded["q"][0])
print("페이지:", decoded["page"][0])
```

macOS/Linux는 `python3 url_check.py`, Windows는 `py url_check.py`로 실행합니다. 예상 결과입니다.

```text
주소: https://example.invalid/search?q=%ED%95%9C%EA%B8%80+%26+%EA%B3%B5%EB%B0%B1&page=2
호스트: example.invalid
검색어: 한글 & 공백
페이지: 2
```

서버 요청 전에는 스킴과 호스트가 허용 목록과 정확히 맞는지 확인하세요. 포트가 허용 범위인지, 사용자 이름·비밀번호 구성 요소가 비어 있는지도 정책에 따라 검사합니다.

[흔한 실수]
검색어를 `?q=` 뒤에 그대로 붙이면 `&`, `=`, `#`가 데이터가 아니라 구분자로 처리될 수 있습니다. URL 전체를 인코딩하거나 쿼리의 `+` 규칙을 경로에 쓰는 것도 잘못입니다. 디코딩 결과를 다시 조립할 때 원래 입력과 완전히 같은 문자열이 될 것이라고 가정하지 마세요.

[보안 주의]
본인 소유의 로컬·격리 환경에서만 실습하세요. 서버가 외부 URL을 가져오는 기능은 HTTPS와 승인한 호스트만 허용하고, 파싱 뒤 사설·루프백 등 금지 목적지를 차단해야 합니다. 리다이렉트 목적지도 다시 검사하고 응답 크기와 시간을 제한합니다. 사용자 입력 URL을 로그나 오류 화면에 그대로 출력하지 말고 개인정보와 자격증명 구성 요소를 가립니다.

[직접 해볼 과제]
검색어를 `C++ 기초/실습`으로 바꾸고 생성된 쿼리와 복원값을 확인하세요. 이어 허용 호스트를 `example.invalid` 하나로 정해 `https`와 호스트가 모두 맞을 때만 `허용`을 출력하는 함수를 작성하세요. 네트워크 요청은 보내지 않습니다.

[확인문제]
1. 값 안의 `&`를 퍼센트 인코딩해야 하는 이유는 무엇입니까?
2. 쿼리와 경로에 같은 인코딩 함수를 무조건 쓰면 안 되는 이유는 무엇입니까?
3. URL을 파싱한 뒤에도 별도 허용 목록 검사가 필요한 이유는 무엇입니까?

[다음 학습]
다음에는 프로그램의 시간 제한을 벽시계가 아닌 단조 시계로 측정해 시각 변경에도 안전하게 만드는 법을 배웁니다.

[공식 참고 자료]
- IETF RFC 3986 URI 문법: https://www.rfc-editor.org/rfc/rfc3986.html
- WHATWG URL 표준: https://url.spec.whatwg.org/
- HTML 폼 URL 인코딩 알고리즘: https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
- Python urllib.parse 공식 문서: https://docs.python.org/3/library/urllib.parse.html
- OWASP SSRF 방어 지침: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html

댓글목록

등록된 댓글이 없습니다.

회원로그인

회원가입

사이트 정보

회사명 : 회사명 / 대표 : 대표자명
주소 : OO도 OO시 OO구 OO동 123-45
사업자 등록번호 : 123-45-67890
전화 : 02-123-4567 팩스 : 02-123-4568
통신판매업신고번호 : 제 OO구 - 123호
개인정보관리책임자 : 정보책임자명

접속자집계

오늘
4,738
어제
6,862
최대
16,772
전체
770,689
Copyright © 소유하신 도메인. All rights reserved.