curl은 403을 반환하지만 브라우저는 작동합니다: 누락된 부분 찾기

브라우저는 로드됩니다. curl은 403을 받습니다. 이 두 요청 간의 차이는 항상 유한하며 항상 찾을 수 있습니다 — 여기서 5분 안에 이를 이등분하는 방법을 소개합니다.

URL을 Chrome에 붙여넣으면 페이지가 로드됩니다. 동일한 URL을 curl에 붙여넣으면 403 Forbidden이 반환됩니다. 이 두 초 사이에 리소스에 대한 변경은 없었으므로 차이는 전적으로 요청에 있습니다 — 그리고 요청은 유한하고 검사 가능한 것입니다. 이 가이드는 이등분 절차입니다: 브라우저가 보낸 것을 정확히 재생한 다음, 403이 다시 나타날 때까지 조각을 제거합니다. 마지막으로 제거한 것이 답입니다. 일반적으로 유죄인 순서로 용의자는 User-Agent, Referer, 쿠키, TLS 지문 및 JavaScript입니다.

0단계: 요청이 실제로 다른지 증명하십시오

이론을 세우기 전에 curl이 실제로 보내는 것을 확인하십시오. -v를 사용하면 요청 라인, 모든 헤더 및 TLS 핸드셰이크를 볼 수 있습니다. 기본 curl 요청은 놀랍도록 얇습니다 — 일반적으로 Host, User-Agent: curl/8.xAccept: */*입니다. 브라우저는 더 많은 헤더를 보냅니다.

# what you send, what you get back, and the TLS details
curl -v -o /dev/null https://target.example/page

# just the response headers, quickly
curl -sS -o /dev/null -D - https://target.example/page

응답 헤더를 상태 라인만큼 주의 깊게 읽으십시오. 가장 일반적인 경우 중 하나는 Vary: User-Agent가 서버가 당신이 누구인지에 따라 다른 응답을 의도적으로 제공한다는 것을 의미합니다. 잘 문서화된 Stack Overflow 사례에서, curl -f가 평범한 Apache 2.4.38 호스트에 대해 403을 반환한 반면 wget은 동일한 파일을 200으로 가져왔고 성공적인 응답은 정확히 그 Vary: User-Agent 헤더를 포함하고 있었습니다. curl에 -A 'Wget/1.21.2'를 전달하면 즉시 해결되었습니다. 사이트 소유자는 남용 후 curl의 사용자 에이전트를 블랙리스트에 올렸으며, 요청의 다른 부분은 중요하지 않았습니다.

출력을 읽는 동안: curl: (22) 요청된 URL이 오류를 반환했습니다: 403은 별도의 문제가 아닙니다. 종료 코드 22는 -f/--fail이 모든 HTTP 오류에 대해 수행하는 것입니다 — 이 플래그는 본문을 억제하고 명령을 실패하게 만듭니다. -f를 일시적으로 제거하여 실제로 차단 페이지를 읽을 수 있으며, 이는 일반적으로 당신을 멈춘 시스템의 이름을 제공합니다.

1단계: cURL로 복사, 30초 해결책

두 주요 브라우저는 방금 만든 정확한 요청을 제공합니다. DevTools를 열고, 네트워크 탭으로 이동하여 요청을 마우스 오른쪽 버튼으로 클릭하고 "cURL로 복사"를 선택하십시오. Chrome은 버전 26부터, Firefox는 31부터 이를 제공하며, 출력에는 모든 헤더, 모든 쿠키 및 referer가 포함됩니다. 이를 터미널에 붙여넣으십시오: 200이 반환되면, 문제는 요청 형태에 확실히 있으며, 2단계에서 어느 부분인지 찾습니다.

여기서 많은 시간을 낭비하는 한 가지 함정이 있습니다. URL이 리디렉션되면, 네트워크 패널이 탐색 시 지워지고 잘못된 요청을 복사하게 됩니다. Chrome에서는 "로그 보존"을, Firefox에서는 "영구 로그"를 먼저 선택하여 리디렉션된 요청과 최종적으로 콘텐츠를 제공한 요청을 모두 볼 수 있도록 하십시오. 리디렉션 체인은 중요합니다: 잘 알려진 Unix Stack Exchange 스레드에서 서버는 Referer를 확인한 후 아무것도 확인하지 않는 위치로 302를 통해 바운스했습니다 — 전체 체인이 보일 때까지 실패가 무작위로 보였습니다.

Chrome 브라우저 요청과 기본 curl 요청의 헤더 수, 쿠키, TLS 지문 및 JavaScript 지원을 나란히 비교
이 시나리오의 모든 403은 이 간격 안에 숨겨져 있습니다. 한 번에 한 열씩 닫으십시오.

2단계: 헤더 이등분

작동하는 "cURL로 복사" 명령에서 시작하여 헤더를 하나씩 삭제하고 각 삭제 후 다시 실행하십시오. 403을 다시 가져오는 첫 번째 삭제가 범인을 지목합니다. 실제로는 거의 항상 네 가지 중 하나입니다.

curl -sS -o /dev/null -w '%{http_code}\n' \
  -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
  -H 'Accept-Language: en-GB,en;q=0.9' \
  -e 'https://target.example/' \
  -b 'session=abc123; consent=1' \
  -L \
  'https://target.example/page'

이 단계의 두 가지 마무리 세부 사항. URL을 인용하십시오: & 또는 액세스 토큰이 포함된 쿼리 문자열은 그렇지 않으면 셸에 의해 망가져서 결과 403이 서버와 관련이 없습니다. 그리고 PHP 또는 Node에서 디버깅하는 경우, 동일한 헤더 세트를 복제하십시오 — PHP 내부의 libcurl 기본값은 명령줄 도구와 다르며, 이는 동일한 요청이 터미널에서는 통과하고 코드에서는 실패하는 이유입니다. 우리의 curl 프록시 레시피는 플래그 구문을 완전히 다룹니다.

3단계: 동일한 헤더가 여전히 403을 반환할 때

브라우저의 헤더를 바이트 단위로 복사해도 실패하면, 결정은 헤더가 구문 분석되기 전에 이루어졌습니다. 그 아래에는 두 개의 레이어가 있습니다.

TLS 지문. 당신의 ClientHello — 암호 스위트, 확장, 곡선 선호도, ALPN, 그리고 그 뒤를 따르는 HTTP/2 설정 프레임 — JA3 또는 JA4 값으로 해시됩니다. OpenSSL을 기반으로 빌드된 curl은 브라우저가 절대 생성하지 않는 것을 생성하며, 안티봇 시스템은 이를 주장된 User-Agent와 비교합니다. OpenSSL처럼 핸드셰이크하면서 Chrome이라고 주장하는 것은 그들이 잡도록 설계된 모순입니다. 해결책은 브라우저 핸드셰이크를 재현하는 클라이언트입니다: 명령줄의 curl-impersonate 또는 Python의 curl_cffi입니다.

# pip install curl_cffi
from curl_cffi import requests

proxy = "http://USER:PASS@gate.quantumproxies.io:8000"

r = requests.get(
    "https://target.example/page",
    impersonate="chrome",          # browser ClientHello + HTTP/2 settings
    proxies={"http": proxy, "https": proxy},
    timeout=20,
)
print(r.status_code, r.headers.get("content-type"))

당신의 IP. 작동하는 브라우저는 보통 가정 연결에 있고 curl은 VPS에서 실행됩니다. 호스팅 ASN은 공개되어 있으며 사전 점수가 매겨져 있으므로, 주거 주소에서 동일한 요청이 읽히기 전에 다르게 판단됩니다. 출구를 교체하는 것은 주거 프록시로 한 줄의 변경입니다 — 90M+ IP가 200+ 국가에 걸쳐 있으며, 모든 플랜에서 HTTP 및 SOCKS5를 제공합니다 — 그리고 네트워크를 배제하는 가장 빠른 방법입니다. 지문 레이어의 메커니즘은 JA3/JA4 TLS 지문에 있습니다.

주거 프록시로 네트워크를 배제하십시오

curl 403에서 200 응답으로의 결정 트리 흐름: cURL로 복사, 헤더 이등분, TLS 모방, 그런 다음 렌더링 또는 스크래퍼 API 사용
네 단계, 각각 하나의 테스트. 상태가 200으로 바뀌면 즉시 멈추십시오 — 필요한 것 이상으로 오르지 마십시오.

4단계: 페이지가 클라이언트가 아닌 브라우저를 필요로 할 때

때로는 403이 당신에 대한 판단이 전혀 아닙니다 — 그것은 당신이 시도하지 않은 챌린지의 실패 모드입니다. npmjs.com을 타격하는 링크 체커에 대한 공개 GitHub 토론은 이를 명확히 합니다: curl은 유효한 챌린지 솔루션을 생성할 수 없으므로 요청이 403으로 차단됩니다. 서버는 작은 JavaScript 문제를 발행하고, 답변을 잠시 기다리며, 이를 실행할 수 없는 모든 것을 거부합니다. 헤더 세트, 지문 및 IP는 코드 실행이 필요한 테스트를 통과하지 못합니다.

그 시점에서 세 가지 솔직한 옵션이 있습니다: 실제 브라우저를 구동하고 비용을 지불하거나, 페이지 자체가 호출하는 JSON 엔드포인트를 찾거나 (이미 열려 있는 네트워크 탭에 종종 있습니다), URL을 필요할 때 렌더링하는 서비스에 전달하십시오. QuantumProxies Scraper API는 마지막을 수행합니다 — 브라우저 등급 TLS, 주거 출구, 페이지가 필요할 때만 JavaScript 렌더링, 그리고 하나의 요청에서 마크다운, JSON 또는 원시 HTML을 반환합니다. 얻은 것이 금지된 페이지가 아닌 빈 페이지라면, 그것은 다른 진단입니다: 스크래퍼가 빈 페이지를 반환하는 이유를 참조하십시오. 그리고 차단 페이지에 Cloudflare Ray ID가 있다면, Cloudflare 오류 1020으로 이동하십시오.

자주 묻는 질문

왜 curl은 403을 받고 내 브라우저는 그렇지 않습니까?

왜냐하면 curl은 대략 세 개의 헤더, 쿠키 없음, referer 없음 및 비브라우저 TLS 지문을 보내는 반면, 브라우저는 열두 개의 헤더, 쿠키 저장소 및 Chrome 핸드셰이크를 보내기 때문입니다. 서버는 리소스가 아닌 요청을 거부하고 있습니다. 브라우저의 정확한 요청을 "cURL로 복사"로 재생한 다음, 헤더를 하나씩 제거하여 어떤 차이가 중요한지 찾으십시오.

왜 wget은 성공하고 curl은 403을 받습니까?

거의 항상 User-Agent 때문입니다. 일부 서버는 남용 후 curl의 UA를 특정적으로 블랙리스트에 올리지만 wget의 UA는 그대로 두며 — 문서화된 사례는 서버가 이를 기준으로 분기한다는 것을 확인하는 Vary: User-Agent 응답 헤더를 보여주었고, curl -A 'Wget/1.21.2'가 200을 복원했습니다. wget은 또한 기본적으로 Accept-EncodingConnection을 보내며, 이는 가끔 중요합니다.

curl에서 User-Agent를 설정하는 방법은 무엇입니까?

-A 'string' 또는 동등한 -H 'User-Agent: string'을 사용하십시오. 잘린 Mozilla/5.0보다 완전하고 최신의 브라우저 문자열을 선호하십시오, 일부 서버는 이제 두 개의 토큰만 보내는 실제 브라우저가 없기 때문에 이를 거부합니다. 전체 세트가 일관성을 유지하도록 일치하는 AcceptAccept-Language 값을 함께 사용하십시오.

curl 오류 22는 무엇을 의미합니까?

종료 코드 22는 서버가 HTTP 오류를 반환할 때마다 -f/--fail에 의해 생성되며, 메시지는 상태를 인용합니다 — 일반적으로 403입니다. 이는 보고 플래그이며, 별개의 결함이 아닙니다. -f를 제거하여 응답 본문을 확인하십시오, 이는 일반적으로 종료 코드보다 차단을 훨씬 더 잘 설명합니다.

프록시가 curl 403을 해결할 수 있습니까?

이는 IP 평판이나 지리적 위치로 인한 하위 집합을 해결합니다 — 스크립트가 클라우드 호스트에서 실행되고 브라우저는 그렇지 않을 때 큰 하위 집합입니다. 이는 누락된 referer, 부재한 쿠키 또는 JavaScript 챌린지를 해결하지 않습니다. 헤더를 먼저 테스트하십시오, 이는 비용이 들지 않으므로, 그런 다음 네트워크 레이어를 분리하기 위해 출구 IP를 변경하십시오.

여기에는 신비가 없고, 단지 간격이 있습니다: 브라우저는 하나의 요청을 보냈고 당신은 다른 요청을 보냈습니다. 브라우저의 요청을 복사하고, 그것이 깨질 때까지 축소하면, 항상 중요한 부분을 찾을 것입니다 — 보통 헤더, 때로는 지문, 가끔은 실제 브라우저가 답해야 하는 챌린지입니다.

Scraper API로 모든 페이지 가져오기