문서 작업 가이드
바깥 링크를 깨뜨리지 않고 URL 쿼리 값 하나 인코딩하기
검색어, callback, 중첩 redirect를 RFC 3986 쿼리 값 하나로 인코딩하면서 바깥 URL의 파라미터 경계와 리터럴 +를 보존합니다.
작성 및 검수 SimpleWebUtils게시일: 검토일:
이 작업 흐름을 검증한 방법
이번 검증은 “중첩 검색 URL을 값 하나로 인코딩”의 정확한 입력과 URL 인코더·디코더를 짝지어 수행했습니다. “가공하지 않은 원본 값 보관” 단계를 거쳐 “수신 규칙 선택” 이후 결과를 캡처해 예시와 비교했습니다. 명시적인 경계 질문은 “바깥 key까지 값과 함께 인코딩”, “확인 없이 원시 +를 공백으로 처리”였습니다.
중첩 검색 URL 값만 인코딩해 문서의 %3A·%2F·%3F·%20·%26 시퀀스를 만들었고, 바깥 파라미터 구조는 수신기가 읽을 수 있게 남겼습니다.
문제 상황
쿼리 문자열에서 `?`, `&`, `=`, `#`, `%`, 때로는 `+`가 문법으로 쓰입니다. 같은 문자가 검색어, callback, filter, redirect 대상 안에서는 데이터일 수 있습니다. 중첩된 앰퍼샌드를 그대로 붙이면 바깥 파라미터가 새로 시작되고, 기존 `%20`을 다시 인코딩하면 `%2520`이 되며, 리터럴 +를 폼 규칙으로 디코딩하면 공백이 될 수 있습니다. 신뢰할 수 있는 작업 단위는 구분하지 않은 전체 URL이 아니라 정확히 식별한 값 하나입니다.
공식 출처와 표준
다음 공식 문서는 이 작업 흐름에서 다루는 형식과 보안 경계의 기준입니다. 도구별 실행 검증은 위에 별도로 기록합니다.
- URL Living Standard
WHATWG
이럴 때 사용하세요
- `next`, `returnUrl`, `redirect_uri`, `callback`, `state` 파라미터 안에 다른 전체 URL이 들어 있습니다.
- 검색어나 filter 값에 공백, `&`, `=`, `#`, `%`, 리터럴 `+`, 한글, 일본어, 이모지가 있습니다.
- 브라우저에서는 링크가 열리지만 수신 애플리케이션이 값을 중간에서 자르거나 여러 파라미터로 읽습니다.
- webhook, API client, OAuth 흐름, QR 목적지에 재현 가능한 인코딩 값 하나가 필요합니다.
- RFC 3986 컴포넌트와 application/x-www-form-urlencoded 폼 규칙 중 무엇을 쓸지 결정해야 합니다.
단계
- 1단계
가공하지 않은 원본 값 보관
편집하기 전에 정확한 파라미터 값을 손대지 않은 상태로 보관합니다. 나중에 비교할 수 있도록 앞뒤 공백, 리터럴 +, 기존 `%HH`를 그대로 둡니다.
- 2단계
바깥 URL과 값 분리
URL Parser 또는 Query String Parser로 문제가 있는 key 하나를 찾습니다. 컴포넌트 입력에는 바깥 `key=` 접두사나 옆의 `&other=value`를 포함하지 않습니다.
- 3단계
수신 규칙 선택
일반 쿼리 값이나 중첩 URL은 RFC 3986 컴포넌트를 사용합니다. 원시 `+`를 공백으로 해석하는 application/x-www-form-urlencoded 규칙을 수신 시스템이 명시할 때만 폼 값을 선택합니다.
- 4단계
정확한 값을 한 번 인코딩
인코딩을 한 번 실행합니다. 자동으로 다시 처리하지 말고 기존 퍼센트 경고를 확인하세요. `%25` 증가는 다른 계층이 추가된 증거입니다.
- 5단계
바깥 링크 재구성 후 다시 파싱
원래 key 뒤에 인코딩 결과를 넣고 전체 링크를 다시 만든 다음 다시 파싱합니다. 바깥 key 개수가 예상과 같고 중첩 앰퍼샌드가 더 이상 분리되지 않는지 확인합니다.
- 6단계
실제 수신 환경에서 검증
권한이 있는 테스트 환경에서만 링크를 열고 애플리케이션이 받은 값과 보관한 원본을 비교합니다. 퍼센트 인코딩은 목적지를 검증하거나 URL의 비밀 값을 보호하지 않습니다.
예시
중첩 검색 URL을 값 하나로 인코딩
입력
https://app.example.com/search?q=jwt decoder&sort=recent출력
https%3A%2F%2Fapp.example.com%2Fsearch%3Fq%3Djwt%20decoder%26sort%3Drecent흔한 실수
바깥 key까지 값과 함께 인코딩
`next=`까지 인코딩하면 수신 애플리케이션이 기대하는 key가 없어질 수 있습니다. 값만 인코딩한 뒤 바깥 key에 연결하세요.
중첩 URL에 전체 URL 모드 사용
전체 URL 모드는 `?`와 `&`를 활성 상태로 둡니다. 최상위 URL을 읽기 좋게 만들 때는 유용하지만 URL 전체가 바깥 값 하나여야 할 때는 맞지 않습니다.
확인 없이 원시 +를 공백으로 처리
컴포넌트 규칙은 원시 `+`를 보존하고 폼 규칙은 공백으로 해석합니다. 프로필 선택이나 치환 전에 수신 규칙을 확인하세요.
기존 escape를 자동으로 다시 인코딩
기존 `%2F`는 한 번 더 인코딩하면 `%252F`가 됩니다. 주변 프로토콜이 요구할 때만 그 계층을 추가하세요.
자격 증명이나 token을 URL에 넣기
인코딩은 기록, 로그, 분석, referrer, 화면 캡처, 링크 수신자에게 데이터를 숨기지 않습니다. 가능하면 URL이 아닌 비밀 전송 방식을 사용하세요.
자주 묻는 질문
key, value, 전체 URL 중 무엇을 인코딩하나요?
이 절차에서는 값 하나를 식별해 인코딩합니다. 바깥 key와 쿼리 구분자는 활성 상태로 둡니다. 전체 URL 자체가 값일 때만 그 URL 전체를 컴포넌트로 인코딩합니다.
공백은 `%20`과 `+` 중 무엇이 되어야 하나요?
RFC 3986 컴포넌트는 `%20`, 폼 값은 `+`를 만듭니다. 전역 치환하지 말고 수신 시스템이 문서화한 규칙을 사용하세요.
리터럴 + 기호는 어떻게 보존하나요?
컴포넌트 인코딩은 리터럴 `+`를 `%2B`로 바꿉니다. 폼 값도 리터럴 +는 `%2B`로 직렬화하고, 폼 디코딩 입력의 원시 `+`만 공백을 뜻합니다.
왜 `%20`이 `%2520`이 되었나요?
다른 인코딩 단계에서 퍼센트 기호가 인코딩됐기 때문입니다. 두 번째 계층이 필요한지 결정하기 전에 어떤 계층이 값을 소유하는지 확인하세요.
요청을 보내지 않고 결과를 확인할 수 있나요?
재구성한 URL을 파싱하고 식별한 값을 한 번 디코딩해 원본과 비교할 수 있습니다. framework마다 쿼리 규칙이 다르므로 최종 동작은 권한 있는 수신 환경에서 확인해야 합니다.
인코딩하면 redirect가 안전해지나요?
아니요. 전송 경계만 보존합니다. 허용 scheme, host, path, signature, 권한은 애플리케이션이 별도로 검증해야 합니다.