문서 작업 가이드
이중 인코딩된 쿼리 값 하나를 계층별로 복구하기
증명된 쿼리 값 계층을 한 번씩 디코딩하고 `%25`와 의도적인 escape를 구분한 뒤 보호된 구분자를 활성화하지 않고 바깥 URL을 다시 만듭니다.
작성 및 검수 SimpleWebUtils게시일: 검토일:
이 작업 흐름을 검증한 방법
가이드 입력을 바꾸지 않은 채 URL 인코더·디코더에서 다음 예제를 재현했습니다: “인코딩된 path 계층 하나 드러내기”. 정상 절차는 “계층을 소유한 쿼리 값 식별”, “남은 계층의 소유자 확인”; 경계 검토는 “전체 URL을 반복문으로 디코딩”, “검증 전에 signed data 변경”로 분리해 결과를 확정했습니다.
한 번의 디코딩은 %252Fdocs%2520v1%252Fapi%253Fpage%253D2를 아직 인코딩된 %2Fdocs%20v1%2Fapi%3Fpage%3D2 계층으로 바꿔 멈출 지점을 분명히 했습니다.
문제 상황
이중 인코딩은 계층 문제이지 전체 URL을 반복 디코딩하라는 뜻이 아닙니다. `%252F`는 한 번 처리하면 `%2F`, 한 번 더 처리해야 `/`가 됩니다. 첫 결과가 바깥 프로토콜에 필요한 정확한 표현일 수 있고, 두 번째 처리는 `/`, `?`, `&`, `=`, `#`를 활성화해 routing이나 파라미터 경계를 바꿀 수 있습니다. signed callback과 OAuth state는 signature 검증 전에 어느 계층이라도 달라지면 실패할 수 있습니다. 복구하려면 원시 증거를 보존하고 값 하나를 분리하며 각 계층의 소유자를 증명해야 합니다.
공식 출처와 표준
다음 공식 문서는 이 작업 흐름에서 다루는 형식과 보안 경계의 기준입니다. 도구별 실행 검증은 위에 별도로 기록합니다.
- URL Living Standard
WHATWG
이럴 때 사용하세요
- 쿼리 값에 `%252F`, `%2520`, `%253A`처럼 `%25` 뒤에 16진수 두 자리가 있습니다.
- callback이나 webhook이 slash 또는 path 대신 리터럴 `%2F` 텍스트를 받습니다.
- proxy, SDK, CDN, form library, template이 이미 퍼센트 인코딩된 값을 다시 인코딩했습니다.
- OAuth, SSO, signed redirect가 여러 전송 계층을 지난 뒤 실패합니다.
- 운영 설정을 바꾸기 전에 통제된 1회 변환 결과를 비교해야 합니다.
단계
- 1단계
정확한 원시 증거 수집
가장 이른 로그에서 전체 URL이나 payload를 브라우저 정규화, 자르기, 반복 디코딩 없이 복사합니다. 비밀 값은 별도의 작업 복사본에서만 가립니다.
- 2단계
계층을 소유한 쿼리 값 식별
바깥 URL을 파싱하고 `%25`가 있는 정확한 key를 찾습니다. 인접 key, 반복 key, fragment를 분리해 그 구분자가 데이터처럼 디코딩되지 않게 합니다.
- 3단계
디코딩 전 문법 검사
모든 `%` 뒤에 16진수 두 자리가 있는지 확인합니다. 잘못된 escape와 유효한 `%HH`가 잘못된 UTF-8을 이루는 경우는 다른 실패이므로 어느 쪽도 조용히 고치지 않습니다.
- 4단계
분리한 값을 한 번 디코딩
값 하나에 RFC 3986 컴포넌트 디코딩을 사용합니다. 도구는 자동으로 다른 계층을 바꾸지 않고 한 번 처리한 뒤 유효한 escape가 남으면 경고합니다.
- 5단계
남은 계층의 소유자 확인
결과를 기대 원본 및 프로토콜 문서와 비교합니다. 남은 `%HH`가 같은 분리 값에 속하고 그 단계에서 수신자가 활성 구분자를 기대할 때만 다시 디코딩합니다.
- 6단계
생성 지점 수정 후 재검증
여분의 encode 호출을 소유한 애플리케이션, SDK, proxy, template에서 제거합니다. 바깥 URL을 한 번 다시 만들고 권한 있는 환경에서 signature, allowlist, routing, 수신 값을 확인합니다.
예시
인코딩된 path 계층 하나 드러내기
입력
%252Fdocs%2520v1%252Fapi%253Fpage%253D2출력
%2Fdocs%20v1%2Fapi%3Fpage%3D2흔한 실수
전체 URL을 반복문으로 디코딩
반복 처리는 바깥 구분자를 활성화하고 책임 계층을 찾는 데 필요한 증거를 없앨 수 있습니다. 값 하나를 분리해 한 번 실행하세요.
남은 escape를 모두 오류로 단정
slash나 물음표 escape가 다음 전송 경계를 위해 의도적으로 보호된 것일 수 있습니다. 각 계층을 소유자와 연결한 뒤 변경하세요.
검증 전에 signed data 변경
OAuth state, webhook signature, signed URL은 정확한 바이트에 의존할 수 있습니다. 원본을 보존하고 프로토콜이 정의한 계층에서 검증하세요.
+ 기호를 전역 치환
원시 +가 공백인 것은 폼 값 규칙에서만입니다. RFC 3986 컴포넌트 데이터에서는 리터럴 +일 수 있어 전역 치환하면 값이 손상됩니다.
생성 지점 대신 관찰된 URL만 수정
수동 정리는 한 번의 증상만 처리합니다. 추가 계층을 만든 애플리케이션, SDK, proxy, template에서 중복 encode를 제거하세요.
자주 묻는 질문
`%25`가 항상 이중 인코딩 증거인가요?
아니요. `%25`는 리터럴 퍼센트 기호의 올바른 인코딩입니다. 한 번 디코딩했을 때 다음 계층이 해석해야 했던 `%HH` 텍스트가 나타나면 다른 계층을 의심할 수 있습니다.
몇 번 디코딩해야 하나요?
증명된 계층마다 한 번, 분리한 값에만 실행합니다. 매 단계에서 멈추고 기대 값과 비교한 뒤 남은 escape의 소유자를 찾으세요.
전체 URL 디코딩 후 일부 `%HH`가 왜 남나요?
전체 URL 디코딩은 URL 구조를 바꾸지 않도록 예약 구분자 escape를 유지할 수 있습니다. 쿼리 값 하나를 분리한 뒤에는 컴포넌트 디코딩이 적합합니다.
OAuth callback signature도 고칠 수 있나요?
인코딩 계층 불일치를 드러낼 수는 있지만 signature를 검증하거나 다시 만들지는 못합니다. signed byte를 정확히 보존하고 provider의 canonicalization 규칙을 따르세요.
디코더가 잘못된 UTF-8이라고 하면 어떻게 하나요?
`%HH` 문법은 완전하지만 바이트가 유효한 UTF-8이 아닐 수 있습니다. 문서화된 문자 인코딩을 확인하고 바이트를 조용히 바꾸지 마세요.
영구 수정은 어디에 적용하나요?
이미 인코딩된 값을 다시 인코딩한 계층, 보통 애플리케이션 helper, SDK, proxy rewrite, form serializer, template에서 수정합니다. 필요한 전송 경계마다 encode 한 번만 유지하세요.