문서 작업 가이드
Query 순서와 encoding 증거를 잃지 않고 callback URL 디버깅하기
실패한 callback URL을 보존하고 브라우저 정규화, 순서가 있는 반복 query 항목, 중첩 return target을 확인한 뒤 필요한 encoding만 수정합니다.
작성 및 검수 SimpleWebUtils게시일: 검토일:
이 작업 흐름을 검증한 방법
“중첩 target과 반복 state를 보존하며 callback 정규화” 확인에는 가이드 원본 데이터와 URL 파서 및 분석기의 “입력과 브라우저 정규화 비교하기” 절차를 사용했습니다. 출력은 문서 결과와 일치해야 했습니다. 기록 전 경계 증거는 “원문을 남기기 전에 수정하기”, “정규화 text를 signature 원문으로 사용하기” 항목별로 검토했습니다.
정규화된 callback은 host를 소문자로 바꾸고 HTTPS 기본 포트 443을 제거하면서, 인코딩된 중첩 target·state 두 값·#complete fragment를 모두 유지했습니다.
문제 상황
Callback URL 한 줄에는 브라우저 URL 정규화, path 해석, form 방식 query decoding, 반복 key, 하나의 value 안에 중첩된 URL, HTTP server로 전송되지 않는 fragment가 함께 들어갑니다. 이 계층을 보존하기 전에 link를 수정하면 client, proxy, SDK, backend 중 어디에서 값이 바뀌었는지 보여 주는 유일한 증거를 없앨 수 있습니다. 이 절차는 원문을 유지하고 raw와 decoded 보기를 비교한 뒤 고장으로 확인된 구성 요소만 수정합니다.
공식 출처와 표준
다음 공식 문서는 이 작업 흐름에서 다루는 형식과 보안 경계의 기준입니다. 도구별 실행 검증은 위에 별도로 기록합니다.
- URL Living Standard
WHATWG
이럴 때 사용하세요
- Link가 눈으로는 맞아 보이는데 OAuth 또는 SSO callback의 redirect_uri 비교가 실패합니다.
- next, returnUrl, continue, redirect parameter 안에 다른 URL이 있고 그 ampersand나 fragment가 잘못된 계층에서 분리됩니다.
- Server framework가 반복된 state, scope, filter, tag key 중 첫 값이나 마지막 값만 유지합니다.
- 검색, campaign, support URL에 +, %2B, %20, 빈 값, 잘못된 percent escape가 섞여 의미가 불분명합니다.
- 상대 callback이나 deep link가 한 base path에서는 동작하지만 proxy 또는 subdirectory 뒤에 배포한 뒤 다르게 해석됩니다.
단계
- 1단계
실패한 값을 정확히 보존하기
수정하기 전에 원본 URL을 비공개 ticket이나 로컬 note에 보관합니다. 공유할 사본에서는 실제 token, authorization code, email, 자격 증명을 제거하되, 보호된 증거에는 separator, percent escape, 반복 key, fragment 구조를 유지합니다.
- 2단계
올바른 파싱 문맥 선택하기
값에 scheme이 있으면 절대 URL을 사용합니다. /callback, ../callback, ?state=..., #done은 참조 + 기준 URL을 선택하고 실패한 application이 쓰는 정확한 public HTTP 또는 HTTPS base를 입력합니다. Parser page의 주소를 암묵적인 base로 사용하지 않습니다.
- 3단계
입력과 브라우저 정규화 비교하기
URL Parser를 실행해 원문과 정규화 URL을 비교합니다. Scheme과 hostname 소문자화, Punycode 변환, 기본 port 제거, root slash 추가, 새로 직렬화된 escape를 기록한 뒤 signature나 정확 일치 redirect allowlist에 어느 형태를 쓸지 결정합니다.
- 4단계
모든 query 항목을 순서대로 검사하기
각 raw query 구간과 decoded key·value를 나란히 봅니다. 반복 key, equals 기호가 없는 항목, 빈 key나 value, 무시된 빈 ampersand 구간, URLSearchParams가 공백으로 디코딩하는 raw +를 확인합니다. 아직 반복 값을 합치지 않습니다.
- 5단계
중첩 URL value를 분리해 검증하기
next나 returnUrl에 다른 URL이 있으면 그 decoded value만 복사합니다. 내부의 ?, &, =, #, 공백, 리터럴 +가 전송 중 보존돼야 할 때 URL Encoder and Decoder로 그 component 하나만 처리합니다. 한 값을 고치려고 바깥 URL 전체를 encode하거나 반복 decode하지 않습니다.
- 6단계
한 구성 요소만 다시 만들고 수신 동작 재현하기
문제로 확인된 구성 요소만 바꾸고 바깥 URL을 한 번 다시 만든 다음 다시 파싱합니다. 그 뒤 target framework, proxy, callback log가 실제로 받은 정확한 값과 비교합니다. 로컬 파싱 성공은 network 접속, 소유권, redirect 동작, TLS, 목적지 안전성을 검사하지 않습니다.
예시
중첩 target과 반복 state를 보존하며 callback 정규화
입력
HTTPS://LOGIN.EXAMPLE.COM:443/callback?next=https%3A%2F%2Fapp.example.com%2Fsearch%3Fq%3Durl%2Bparser&state=abc123&state=retry#complete출력
https://login.example.com/callback?next=https%3A%2F%2Fapp.example.com%2Fsearch%3Fq%3Durl%2Bparser&state=abc123&state=retry#complete흔한 실수
원문을 남기기 전에 수정하기
Separator나 escape를 바꾸고 나면 어느 계층에서 오류가 생겼는지 확인할 수 없을 수 있습니다. 먼저 비공개로 보관할 마스킹 사본을 만드세요.
정규화 text를 signature 원문으로 사용하기
브라우저 정규화는 유용하지만 signature, cache key, allowlist가 받은 정확한 byte와 다를 수 있습니다. 두 형태를 비교하고 target system의 canonicalization 문서를 따르세요.
중복 key를 평탄화하기
반복 key는 순서에 민감할 수 있습니다. 수신 framework가 첫 값, 마지막 값, 목록 중 무엇을 쓰는지 확인할 때까지 모든 항목을 유지하세요.
더하기 기호를 모두 치환하기
Query decoding에서 raw +는 보통 공백이고 %2B는 리터럴 +입니다. 전체 치환은 C++, 전화번호, signature, opaque identifier를 손상할 수 있습니다.
실제 비밀 값을 report에 공유하기
자격 증명 마스킹은 query value 안의 OAuth code, JWT, API key, 개인정보를 식별하지 않습니다. Screenshot, ticket, download가 신뢰 환경 밖으로 나가기 전에 해당 값을 가리세요.
자주 묻는 질문
입력이 유효한데도 정규화 URL이 달라지는 이유는 무엇인가요?
브라우저 URL serializer는 scheme과 host를 소문자로 바꾸고, IDN을 Punycode로 바꾸고, root slash를 추가하고, 문자를 percent escape로 직렬화하고, 기본 port를 제거할 수 있습니다. 이는 parser 의미이며 정확 signature 입력을 서로 바꿔 써도 된다는 증거는 아닙니다.
바깥 URL을 파싱하기 전에 중첩 return URL을 decode해야 하나요?
먼저 바깥 URL을 파싱하세요. 그 다음 decoded parameter 하나만 복사해 별도로 검사합니다. 바깥 URL 전체를 decode하면 보호된 ampersand, equals, fragment가 잘못된 계층에서 활성화될 수 있습니다.
Server는 반복 query value 중 어느 값을 사용하나요?
URL 자체는 모든 값과 순서를 보존하지만 framework마다 다릅니다. 첫 값, 마지막 값, 목록을 반환할 수 있으므로 정책을 추정하지 말고 실제 receiver를 재현하세요.
더하기 기호가 왜 공백으로 바뀌었나요?
URLSearchParams는 raw +를 공백으로 해석하는 form query decoding을 적용합니다. 리터럴 +는 보통 %2B로 와야 합니다. 수정하기 전에 raw와 decoded 열을 비교하세요.
Fragment도 callback request에 포함되나요?
# 뒤 fragment는 client가 처리하며 HTTP request target에는 전송되지 않습니다. Client-side application은 읽을 수 있으므로 server parameter와 분리해 기록하세요.
URL Parser가 callback을 전송하거나 목적지를 검증하나요?
아니요. 파싱은 로컬에서 실행되고 이동이나 request를 만들지 않습니다. DNS, TLS, redirect, 소유권, server parsing, 안전성은 알맞은 통제 환경에서 별도로 확인하세요.