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

빌드노트

curl -L 뒤 다른 호스트에서만 401이 난다면

curl -L 뒤 다른 호스트에서 401이 난다면 첫 응답의 Location과 목적지의 Authorization 수신 여부를 함께 본다. curl 8.21.0의 loopback 실험에서 같은 origin의 헤더 유지, 호스트·포트 변경 시 누락, 검증한 목적지 직접 호출의 차이를 확인한다.

curl의 첫 요청에 Authorization을 넣었는데 리다이렉트된 서버에서만 401이 난다면, 이동 전후의 URL부터 비교한다. -L은 새 주소로 요청을 보내지만, 인증 헤더를 모든 목적지에 전달하지는 않는다. 이 글에서 확인한 curl 8.21.0은 URL의 호스트·스킴·포트로 정해지는 origin이 바뀌면, -H로 직접 지정한 Authorization도 기본적으로 전달하지 않는다.

따라서 첫 요청에 헤더가 있었다는 사실만으로 다음 서버가 받았다고 판단할 수 없다. 첫 응답의 Location과 목적지 서버의 헤더 수신 여부를 함께 확인해야 한다.

우선 첫 응답에서 멈춘다

문제가 나는 읽기 전용 테스트 주소에서, 처음에는 -L을 빼고 응답을 확인한다. 아래는 Windows의 PowerShell 7과 curl.exe를 쓰는 명령이다. api.example.test는 설명용 주소이므로 $url을 자신의 테스트 주소로 바꾸고, $authorization에는 그 주소에 사용할 수 있는 테스트 인증값을 넣는다. Bearer lab-only는 실제 토큰이 아닌 합성 문자열이다.

다음 명령으로 설치 버전과 첫 응답의 상태·Location을 확인한다.

curl.exe --version
$url = 'https://api.example.test/old-profile'
$authorization = 'Bearer lab-only'
curl.exe --disable --silent --show-error --max-time 10 --header "Authorization: $authorization" --dump-header - --output NUL $url

--disable은 기본 curl 설정 파일을 읽지 않도록 첫 옵션에 둔 것이다. --dump-header -는 응답 헤더를 보여 주고, Windows의 NUL은 응답 본문을 버린다. 이 명령에는 -L이 없으므로 302와 Location이 나오면 거기서 멈춘다. 처음부터 401이면 아직 이 글의 리다이렉트 뒤 헤더 누락을 확인한 상태가 아니다.

Location이 가리키는 URL에서 호스트 이름뿐 아니라 https와 http, 포트도 비교한다. 상대 경로라면 현재 URL을 기준으로 해석한다. 같은 IP로 연결된다는 이유만으로 같은 origin이라고 판단하지 않는다.

목적지가 허용된 테스트 서버임을 확인했다면, 아래 명령으로 이동 후 상태와 실제 마지막 URL을 확인한다. 앞에서 설정한 두 변수를 그대로 사용한다.

curl.exe --disable --silent --show-error --max-time 10 --location --max-redirs 3 --header "Authorization: $authorization" --dump-header - --output NUL --write-out 'http=%{http_code} redirects=%{num_redirects} final=%{url_effective}\n' $url

리다이렉트가 한 번이고 목적지가 401을 반환했다면 마지막 줄은 http=401 redirects=1 final=목적지URL 형태가 된다. 중간 응답의 Location도 함께 읽는다. 연결 오류나 시간 초과로 끝났다면 먼저 그 실패를 해결해야 하며, 출력만으로 헤더가 빠졌다고 결론 내릴 수 없다. 마지막 URL과 이동 횟수의 의미는 curl의 write-out 설명 (새 탭에서 열림)에 나와 있다.

이 명령이 보여 주는 것은 응답이다. 실제로 보낸 Authorization의 존재 여부는 첫 서버와 목적지 서버에서 각각 확인한다. 테스트 요청을 식별할 경로·시각과 함께 헤더의 존재 여부만 남겨도 이 문제를 구별할 수 있다. 인증값 자체를 로그에 남길 필요는 없다.

호스트만 바꾸자 목적지에서 헤더가 사라졌다

위 동작을 확인하려고 Windows의 curl 8.21.0, PowerShell 7.6.6, Python 3.12.10으로 별도 HTTP 실험을 했다. 새 서버를 loopback에만 열고, Authorization: Bearer lab-only와 X-Lab-Label: trace-only를 보냈다. 프록시는 끄고 GET 요청과 302 응답만 사용했다. 서버는 합성 Authorization 값이 맞으면 200, 없으면 401을 반환하도록 만들었다.

실제 배정된 포트는 1335와 1336이었다. 다음은 각 서버가 기록한 수신 헤더와 최종 상태를 비교한 결과다. 이 주소들은 실험 당시의 것이며 현재 실행 중인 서비스가 아니다.

  • 127.0.0.1:1335/receive 직접 호출: Authorization 수신, 200.
  • 127.0.0.1:1335/same에서 같은 호스트·포트의 /receive로 이동: 첫 요청과 목적지 모두 Authorization 수신, 200.
  • 127.0.0.1:1335/other-host에서 127.0.0.2:1335/receive로 이동: 첫 요청만 Authorization 수신, 목적지는 헤더 없이 401.
  • 127.0.0.1:1335/other-port에서 127.0.0.1:1336/receive로 이동: 호스트는 같아도 목적지에는 헤더가 없었고 401.
  • 실험자가 관리하는 127.0.0.2:1335/receive를 같은 인증값으로 직접 호출: Authorization 수신, 200.

다른 호스트로 이동한 요청의 응답에서 관련 줄만 남기면 다음과 같다. 첫 서버의 수신 기록에는 Bearer lab-only가 있었지만, 목적지 응답 본문은 헤더 부재를 보고했다.

HTTP/1.1 302 Found
Location: http://127.0.0.2:1335/receive
HTTP/1.1 401 Unauthorized
receiver_host=127.0.0.2:1335
authorization=<absent>
x_lab_label=trace-only
CURL http=401 redirects=1 final=http://127.0.0.2:1335/receive

이 서버에서 나온 401은 합성 인증 헤더가 빠진 결과다. 실제 서비스의 모든 401이 같은 원인이라는 뜻은 아니다. 또 X-Lab-Label은 다른 호스트와 포트에도 도착했다. 이 관찰로 임의의 비밀 헤더까지 curl이 자동으로 막아 준다고 일반화할 수는 없다.

설치 버전과 같은 curl 8.21.0의 header 문서 (새 탭에서 열림)는 다른 origin으로 리다이렉트할 때 Authorization과 Cookie를 별도로 제한한다고 설명한다. 같은 버전의 location-trusted 문서 (새 탭에서 열림)에서는 호스트 이름·스킴·포트 중 하나가 달라지는 경우를 경계로 설명한다. 경로만 바뀐 조건과 포트만 바뀐 조건의 차이가 이 문서 계약과 맞았다.

인증을 보낼 주소를 먼저 정한다

--location-trusted는 다른 목적지로도 자격증명을 보내도록 허용한다. 따라서 401을 없애기 위해 곧바로 추가하면, 리다이렉트 응답이 지정한 다른 서버에도 인증값이 전달될 수 있다. 이번 실험에서는 이 옵션을 사용하지 않았다.

Location이 예상하지 못한 호스트나 스킴을 가리킨다면, 그 주소를 만든 리다이렉트 설정부터 바로잡는다. 의도한 API 이전이라면 최종 HTTPS 주소의 운영 주체와 그 API에 맞는 인증값인지 확인한 뒤, 검증한 주소를 직접 호출하도록 클라이언트의 기본 URL을 고칠 수 있다. 위 실험의 직접 호출 200은 새 요청의 최초 목적지에는 헤더가 붙는다는 관찰이며, 현실의 두 API가 같은 토큰을 받아야 한다는 근거는 아니다.

여러 번 이동한다면 각 Location을 확인한다. 처음과 마지막 주소만 같아도 중간에 다른 목적지가 끼어 있을 수 있다. 목적지가 Authorization을 실제로 받았는데도 401이라면, 이 실험의 헤더 누락과는 조건이 다르므로 그 서버의 인증 검증 결과를 조사한다.

실행으로 확인한 범위는 위 HTTP GET·302·명시적 Authorization 헤더 조건이다. HTTPS와 HTTP 사이의 스킴 변경은 버전 문서로 확인했으며 TLS 서버로 실행하지 않았다. Cookie, --user, 다른 인증 방식과 다른 curl 버전도 실험하지 않았다. 온라인 매뉴얼은 확인 당시 8.23.1 기준이었고, 설치된 빌드의 --manual은 비활성화되어 있어 정확한 버전 계약은 8.21.0 태그 문서로 대조했다.

← 전체 글 보기