본문 바로가기
빌드노트 검색

빌드노트

따옴표로 감싼 URL인데 curl이 대괄호를 범위로 해석한다

URL을 인자 하나로 전달해도 curl 자체 URL globbing은 남는다. 실제 curl.exe로 대괄호·중괄호 쿼리의 오류, 요청 수와 수신 대상을 비교하고 -g/--globoff를 선택할 조건을 설명한다.

curl에 ?tags[role]=ops가 붙은 URL을 따옴표로 감싸 전달했는데도 bad range가 나올 수 있다. URL을 인자 하나로 전달했더라도 curl 자체의 URL globbing은 계속 적용된다. 대괄호와 중괄호를 입력 그대로 보내려는 요청에는 -g, 또는 같은 옵션의 긴 이름인 --globoff를 추가한다.

curl은 [1-2]를 범위로, {ops,dev}를 목록으로 해석해 URL 하나에서 여러 전송을 만들 수 있다. [role]처럼 범위 문법에 맞지 않는 입력은 전송 전에 오류가 된다. 이 기능은 셸 확장과 별도로 curl 명령행 도구가 수행한다. curl 공식 URL globbing 설명 (새 탭에서 열림)

같은 URL에 -g를 붙여 비교한다

아래는 Windows 11, PowerShell 7.6.6에서 실제 C:\Windows\System32\curl.exe 8.21.0으로 실행한 비교다. 별칭과 혼동하지 않도록 명령 이름을 curl.exe로 썼다. URL 변수는 문자열 하나로 전달했고, 셸을 거치지 않는 Python 인자 배열 실행에서도 같은 결과를 확인했다. Bash나 다른 셸의 인자 전달 방식까지 실행한 결과는 아니다.

자기 환경에서는 먼저 요청 줄을 기록할 수 있는 로컬 HTTP 테스트 서버를 준비한다. 다음 URL의 주소·포트·/echo를 그 서버의 GET 경로로 바꾸고 합성 쿼리를 붙인다. 여기의 13701은 이번 실험에서 임의로 배정된 포트이며 수신기는 이미 종료했다. 비교할 것은 응답 성공 여부와 함께 실제로 도착한 요청 수와 요청 대상이다.

같은 URL에서 기본 실행이 실패하고 -g를 추가한 실행이 대괄호를 보존하는지 확인한다.

$url = 'http://127.0.0.1:13701/echo?tags[role]=ops'
curl.exe -q --noproxy '*' --max-time 3 -sS -v "$url"
curl.exe -q --noproxy '*' --max-time 3 -sS -v -g "$url"

-q를 첫 옵션으로 둬 기본 curl 설정 파일을 제외하고, 로컬 비교에서는 --noproxy '*'로 프록시를 거치지 않게 했다. 두 실행 사이에서 바꾼 동작은 URL globbing뿐이다. -v 출력 중 > GET ...은 curl이 보내는 요청 줄이다.

첫 실행은 다음 오류를 냈다. 오류 위치 번호는 URL 길이에 따라 달라진다.

curl: (3) bad range in position 34:
http://127.0.0.1:13701/echo?tags[role]=ops

이 실행의 종료 코드는 3이었고 수신 요청은 0건이었다. 서버의 배열형 쿼리 파서가 거절한 것이 아니라, curl이 URL을 해석하다 끝난 조건이다. 두 번째 실행은 종료 코드 0으로 끝났고, curl의 상세 로그에는 > GET /echo?tags[role]=ops HTTP/1.1이 나왔다. 수신기에서도 아래 요청 줄을 정확히 한 번 기록했다.

GET /echo?tags[role]=ops HTTP/1.1

GET과 HTTP/1.1 사이가 요청 대상(request target)이다. 여기서는 쿼리를 객체로 변환하기 전의 요청 줄을 읽었다. 수신기는 그 대상을 응답 본문으로 돌려주도록 만든 단순한 서버이며, 이 결과가 실제 프레임워크의 배열 변환까지 확인한 것은 아니다.

자기 서버에서 연결 거부나 시간 초과가 난다면 먼저 수신기 주소와 실행 상태를 확인한다. -g를 넣어도 연결 오류가 해결되는 것은 아니다. 요청은 도착했는데 앱에서 읽은 값만 다르다면, 원문 요청 대상과 파싱 후 값을 구분해 다음 처리 단계를 조사한다.

오류 없이 두 번 요청하는 경우도 있다

같은 /echo에서 쿼리만 tag={ops,dev}로 바꿨다. 따옴표는 계속 유지했다. 이 입력은 curl의 목록 문법으로 유효하므로 오류 대신 두 전송이 생겼다. 다음은 두 조건에서 수신기가 기록한 요청 줄이다.

기본 실행
GET /echo?tag=ops HTTP/1.1
GET /echo?tag=dev HTTP/1.1

-g 추가
GET /echo?tag={ops,dev} HTTP/1.1

두 조건 모두 curl 종료 코드는 0이었다. 정상 종료만 보고 URL이 그대로 전달됐다고 판단할 수 없는 이유다. 기본 실행은 서버에 ops와 dev를 따로 보냈고, -g 실행은 중괄호가 남은 요청 하나를 보냈다. 서버가 받은 요청 수를 함께 보면 이 차이를 놓치지 않는다.

숫자 범위인 tag=[1-2]에서도 기본 실행은 /echo?tag=1, /echo?tag=2 두 요청이었다. -g를 추가하자 /echo?tag=[1-2] 한 요청이 됐다. 반대로 여러 URL을 의도적으로 순회하려고 범위나 목록을 쓴다면 기본 globbing을 유지할 수 있다.

대괄호가 있다는 사실만으로 오류를 단정하지는 않는다. 같은 curl 8.21.0에서 tags[]=ops는 기본 실행과 -g 실행 모두 /echo?tags[]=ops로 한 번 도착했다. 빈 []가 통과했다는 관찰을 [role]이나 [1-2]의 동작에 적용할 수는 없다. 문제가 난 입력 자체로 비교해야 한다.

인코딩과 IPv6는 구분해서 판단한다

--globoff는 curl의 패턴 해석을 끄는 옵션이다. 문자들을 퍼센트 인코딩하거나, 서버가 그 요청을 수락하게 만드는 옵션은 아니다. 공식 --globoff 매뉴얼 (새 탭에서 열림)은 이 문자들을 URI 규칙에 맞게 인코딩해야 한다고도 설명한다. curl URL 구문 문서 (새 탭에서 열림)는 curl이 사용하는 URL 구문과 다른 파서의 처리가 같지 않을 수 있음을 설명한다.

API에 전달할 정상 쿼리를 구성한다면, 대괄호가 포함된 이름의 인코딩을 검토한다. 예를 들어 tags[role]의 대괄호를 인코딩하면 tags%5Brole%5D가 된다. 같은 수신기에 ?tags%5Brole%5D=ops를 보냈을 때는 기본 실행과 -g 실행 모두 /echo?tags%5Brole%5D=ops가 한 번 도착했다. 전송된 요청 대상에는 인코딩된 표기가 남아 있었다. 이것이 앱에서 tags[role]이라는 이름으로 읽히는지는 서버의 쿼리 디코딩을 따로 확인해야 한다.

원래 문자가 남은 요청을 서버가 어떻게 받는지 재현하려면 인코딩으로 입력을 바꾸기보다 따옴표와 -g를 함께 유지한다. 정상 API 호출용 URL을 만드는 문제와 원문 요청을 재현하는 문제는 이 지점에서 선택이 달라진다.

또한 호스트 부분의 http://[::1]:포트/는 IPv6 주소 표기다. 공식 매뉴얼에 따르면 curl은 숫자 IPv6 주소를 인식해 globbing 대상에서 자동으로 제외한다. 쿼리의 대괄호와 같은 문제로 취급해 주소의 대괄호를 제거하면 안 된다. 이번 수신기는 127.0.0.1에만 열었으므로 IPv6 연결은 실행하지 않았고, 이 예외는 문서에서 확인했다.

확인 당시 온라인 매뉴얼은 curl 8.23.1을 설명했고, 직접 비교한 바이너리는 8.21.0이었다. 관찰 범위는 위 ASCII 쿼리를 보낸 로컬 HTTP/1.1 GET이다. HTTPS, 리다이렉트, 실제 프록시와 프레임워크의 후속 파싱은 검증하지 않았다. 자기 환경에서도 먼저 URL이 인자 하나로 전달되는지 확인하고, 리터럴 전송이 목적이면 --globoff를 추가한 뒤 수신 요청 수와 대상을 대조한다.

← 전체 글 보기