Turnstile 위젯만 달면 우회된다…서버 검증까지 해야 끝난다

|작성자: QUASA 편집팀|5 분 소요| 1
Turnstile 위젯만 달면 우회된다…서버 검증까지 해야 끝난다

Cloudflare Turnstile로 로그인·문의 폼을 보호하려면 브라우저에서 위젯이 만든 토큰을 폼 요청에 싣고, 서버에서 Siteverify로 검증한 뒤에만 요청을 처리해야 한다. Cloudflare의 시작 안내는 위젯 삽입과 서버 검증을 함께 필요한 단계로 제시한다. 화면에 위젯만 표시하고 서버가 토큰을 확인하지 않으면, 폼 화면을 거치지 않고 처리 주소로 직접 전송된 요청을 걸러낼 수 없다.

구현의 기준은 제출 버튼이 활성화됐는지가 아니라 서버가 어떤 요청을 허용하는지다. 서버는 토큰이 없거나 검증에 실패한 요청을 로그인 자격증명 확인, 세션 발급, 문의 저장 또는 알림 발송으로 이어지지 않게 해야 한다. 정상 토큰을 받은 요청에 대해서만 원래 업무 처리를 시작한다. 브라우저에서 버튼을 잠그거나 오류를 표시하는 코드는 사용자 경험에 도움이 되지만, 요청 허용 여부를 결정하는 검사를 대신하지 못한다.

사이트키는 폼에, 비밀키는 서버에 둔다

Cloudflare 대시보드에서 보호할 폼에 사용할 Turnstile 위젯을 만들면 사이트키와 비밀키가 발급된다. 사이트키는 브라우저가 위젯을 표시하는 데 쓰는 공개 식별자다. 비밀키는 서버가 토큰을 검증할 때 Siteverify에 보내는 자격증명이므로 HTML, 프런트엔드 번들 또는 브라우저에 전달되는 설정값에 넣어서는 안 된다.

서버에서는 비밀키를 TURNSTILE_SECRET_KEY 같은 환경변수나 비밀 관리 서비스에서 읽도록 한다. 배포 설정에서 서버 프로세스가 그 값을 읽을 수 있는지 확인하고, 오류 응답이나 로그에 비밀키가 출력되지 않게 한다. 개발·시험·운영 환경의 위젯을 분리하면 시험용 키가 실제 폼에 섞이는 문제도 줄일 수 있다. 프런트엔드에 노출되는 환경변수는 이름에 ‘비밀’이라는 단어가 들어 있어도 비밀 저장소가 아니다.

로그인과 문의 폼을 함께 운영한다면 각 폼이 어느 위젯을 사용하는지 정해 두는 편이 좋다. 서버에서 토큰 검증이 성공해도 그 토큰이 현재 요청의 용도에 맞게 쓰였는지는 별도로 판단해야 한다. 위젯에 용도를 나타내는 action을 지정하고, 검증 응답의 action과 요청 종류를 비교하면 로그인용으로 설정한 토큰을 문의 처리에 사용하는 경우를 구분할 수 있다.

브라우저 토큰을 실제 제출 요청에 포함한다

일반 HTML 폼에서는 Cloudflare가 제공하는 challenges.cloudflare.com/turnstile/v0/api.js 스크립트를 불러오고, form 안에 class="cf-turnstile"과 data-sitekey="사이트키"가 지정된 위젯 요소를 둔다. 기본 폼 통합 설정에서는 위젯이 만든 토큰이 cf-turnstile-response라는 숨김 필드에 담겨 다른 입력값과 함께 제출된다. 사이트키는 실제로 생성한 위젯의 값을 사용하고, 비밀키는 이 요소에 넣지 않는다.

자바스크립트로 제출을 가로채 별도의 비동기 요청을 보내는 폼은 전송 본문을 따로 확인해야 한다. 화면에서 위젯이 정상적으로 보이더라도 비동기 요청에 cf-turnstile-response가 빠지면 서버는 검증할 토큰을 받지 못한다. 반대로 토큰만 먼저 별도 요청으로 검증하고 나중에 폼 데이터를 보내는 구조도 피한다. 사용자 입력값과 토큰을 같은 제출 요청으로 서버에 전달하고, 그 요청을 처리하기 직전에 검증하는 흐름이 명확하다.

위젯이 토큰을 만들기 전까지 제출 버튼을 비활성화할 수는 있다. 다만 브라우저의 버튼 상태나 숨김 필드 값은 요청을 보내는 쪽에서 바꿀 수 있다. 서버는 토큰 필드가 비어 있거나 문자열 형식이 맞지 않으면 Siteverify 호출 여부와 관계없이 해당 업무 처리를 중단해야 한다. 이 검사는 로그인과 문의처럼 보호할 각 처리 주소에 들어가야 한다.

Siteverify 응답을 받은 뒤 원래 요청을 처리한다

Siteverify 검증 문서에 따르면 서버는 challenges.cloudflare.com의 /turnstile/v0/siteverify 경로에 HTTPS POST 요청을 보내고, 필수 항목인 secret과 response에 각각 비밀키와 브라우저 토큰을 담는다. 방문자의 IP를 담는 remoteip는 선택 항목이다. API는 JSON으로 응답하며, 토큰은 생성 후 300초 동안 유효하고 한 번만 검증할 수 있다.

Node.js 로그인 처리라면 요청 본문의 cf-turnstile-response를 token으로 읽고, 서버 환경변수에서 비밀키를 가져오는 순서로 시작할 수 있다. 이어 secret에 비밀키, response에 token을 넣어 Siteverify를 호출한다. 응답의 success가 true일 때만 비밀번호 검사 함수를 실행한다. 검증 요청 자체가 실패했거나 응답을 해석할 수 없다면 success를 추정하지 말고 로그인 처리를 중단한다.

  1. 요청에서 사용자 입력값과 토큰을 읽는다. 토큰이 없으면 계정 조회, 비밀번호 확인, 문의 저장을 시작하지 않는다.
  2. 서버가 보유한 비밀키로 Siteverify를 호출한다. 브라우저가 직접 호출하게 만들면 비밀키를 브라우저에 공개해야 한다.
  3. success가 true이고 서비스가 설정한 추가 조건도 맞을 때만 원래 요청을 처리한다. 조건이 맞지 않으면 성공 응답이나 세션을 만들지 않는다.

여러 도메인이나 용도의 폼을 운영한다면 응답의 hostname과 action을 기대값과 비교한다. hostname은 위젯이 제공된 호스트를 나타내고, action은 위젯에 지정한 용도 식별자다. 예를 들어 로그인 폼에 action을 지정했다면 서버의 로그인 처리 주소는 그 값이 일치하는지도 확인할 수 있다. 이 비교는 위젯 생성 시 허용 도메인을 제한하는 설정과 함께 사용하면 판단 기준이 더 분명해진다.

검증 실패와 네트워크 오류를 구분해 처리한다

토큰이 만료되거나 이미 검증에 사용됐다면 같은 값을 다시 보내도 통과하지 않는다. 이때는 가능하면 사용자가 입력한 폼 내용을 유지하고 위젯에서 새 토큰을 받아 다시 제출하게 한다. 같은 토큰을 반복 전송하는 재시도 버튼은 문제를 해결하지 못한다. 브라우저에 토큰이 남아 있다는 사실만으로 서버의 유효성 검사를 생략해서도 안 된다.

Siteverify의 error-codes는 운영 로그에서 실패 원인을 구분하는 데 쓸 수 있다. missing-input-response는 토큰 누락을, invalid-input-secret은 비밀키 설정 문제를 살펴볼 신호다. timeout-or-duplicate는 만료 또는 이미 사용한 토큰과 관련된다. 이용자에게는 내부 오류 코드나 비밀키를 보여주기보다 새로 검증해 제출할 수 있도록 간결하게 안내한다.

네트워크 오류나 Siteverify 응답 지연도 검증 성공으로 취급하지 않는다. 서버 호출에 시간 제한을 두고, 결과를 받지 못했다면 원래 업무 처리를 보류한다. 일시적인 전송 오류로 동일한 검증 요청을 서버에서 재시도할 때는 idempotency_key를 사용해 그 시도를 식별할 수 있다. 로그에는 오류 유형과 요청 경로처럼 진단에 필요한 맥락을 남기되, 토큰 원문과 비밀키를 기록하지 않는 편이 안전하다.

테스트 키로 허용과 거절 경로를 확인한다

Cloudflare의 테스트 키 안내는 항상 통과하는 사이트키 1x00000000000000000000AA와 테스트용 비밀키 1x0000000000000000000000000000000AA의 조합을 제공한다. 실패 경로에는 사이트키 2x00000000000000000000AB와 비밀키 2x0000000000000000000000000000000AA를 사용할 수 있다. 테스트 사이트키가 만든 더미 토큰은 운영 비밀키로 검증되지 않으므로 브라우저와 서버의 키를 같은 환경에 맞춰 설정해야 한다.

로컬 테스트에서는 정상 토큰으로 폼이 처리되는지만 보지 않는다. 토큰을 아예 빼거나 실패용 키를 사용했을 때 서버가 요청을 거절하는지 확인한다. 로그인이라면 거절된 요청에서 비밀번호 확인과 세션 발급이 실행되지 않아야 한다. 문의 폼이라면 내용 저장과 알림 발송이 일어나지 않아야 한다. 프런트엔드의 오류 표시뿐 아니라 서버의 실제 처리 경로를 함께 확인하는 이유다.

운영 배포 전에는 실제 사이트키와 비밀키가 연결됐는지, 검증 실패를 로그에서 구분할 수 있는지, 만료 후 재제출에서 새 토큰을 받는지 점검한다. 폼의 제출 방식을 일반 HTML 전송에서 비동기 요청으로 바꾸거나 새 처리 주소를 추가할 때도 토큰 전달과 서버 검증 경로를 확인한다. 보호하려는 요청마다 검증 결과가 업무 처리에 반영돼야 위젯을 설치한 목적이 구현된다.

함께 읽기:

공유:

뉴스레터 구독

최신 Web3, AI, 암호화폐 뉴스를 이메일로 받아보세요.

0