페이지의 절반만 렌더링되고, 스타일시트 하나가 네트워크 패널에서 회색으로 남아 있으며, VPN 클라이언트는 여전히 정상 터널을 보고한다. 세 관찰이 동시에 참인 것은 브라우저가 판단한 것이 페이지가 만든 요청이고 그 판단에 터널이 발언권이 없었기 때문이다. 사건을 닫는 것은 설정이 아니라 결정이며, 참조를 고치거나 호스트 소유자에게 에스컬레이션하거나 의존 자체를 버린다. 각 선택지에는 값싼 검증이 있고, 잘못 고르면 지원 주기 한 번을 태운다.

터널이 하위 리소스를 덮을 수 없는 이유

혼합 콘텐츠는평문 HTTP로 하위 리소스를 요청하는 HTTPS 문서로 시작된다. URL은 각자의 스킴으로 판단되므로 부모 문서의 보호가 자식 요청까지 이어지지 않는다. 브라우저 아래에서 더하는 암호화는 회선 위에서 오가는 것을 바꿀 뿐, 마크업이 요구한 http://를 바꾸지는 않는다.

현재 브라우저는 이런 요청을 두 무리로 나눠 다르게 다룬다. 스크립트, 스타일시트, iframe, 그리고 fetch, XMLHttpRequest, WebSocket으로 보낸 호출은 업그레이드를 시도하지도 않고 즉시 거부된다. 이미지, 오디오, 비디오는 https://로 다시 쓰여 재요청되고, 그 안전한 가져오기가 실패하면 로딩이 중단된다. 호스트가 맨 IP 주소인 하위 리소스 URL은 업그레이드 대상에서 빠지므로, IP 호스트의 HTTP 이미지는 조용히 고쳐지는 대신 차단된 채로 남는다. 이 구분은 업그레이드 가능한 쪽의 예외까지 포함해Mozilla의 혼합 콘텐츠 안내서에 문서화되어 있다. Chrome은 안전한 페이지에서 시작된 안전하지 않은 다운로드에도 비슷한 거부를 적용하므로, 주변 페이지가 멀쩡해 보여도 PDF 링크가 죽을 수 있다.

따라서 회색 행은 통제가 작동한다는 증거이지, 터널이 평문을 흘렸다는 증거가 아니다. 그 행은 페이지 전체가 아니라 참조 하나를 가리키는 발견으로 다뤄야 한다.

안전하지 않은 참조의 주인을 정한다

무엇이든 바꾸기 전에 문제 URL을 생성되는 위치로 분류한다. 그 답이 오늘 수정을 낼 수 있는지, 공급업체가 내야 하는지, 아니면 의존을 페이지에서 빼야 하는지를 정한다. 화면의 마크업이 실행 중에 조립되는 경우가 많으므로 네트워크 패널의 Initiator 열에서 읽는다.

URL의 출처누가 고칠 수 있나첫 단계
배포하는 템플릿이나 파셜에 하드코딩됨당신https:// 형식으로 바꾼 뒤 캐시를 비운다
태그 관리자나 공급업체 번들이 주입함공급업체, 당신의 티켓과 함께정확한 URL, 요청 유형, 콘솔 줄을 보낸다
80번 포트에서만 제공되며 TLS 리스너가 없음호스트 소유자TLS를 요청하거나, 직접 관리하는 것을 앞단에 두거나, 버린다

호스트가 이미 443에서 응답하고 참조만 http라고 말한다면 수정은 국소적이므로, 참조를 바꾸고 페이지 캐시를 비우고 행이 https가 되는지 확인한다. 80번 포트만 듣는 호스트는 편집으로 맞출 수 없으므로 리소스는 옮겨 가거나, 직접 관리하는 것이 앞단에 놓이거나, 사라진다. 태그 관리자가 주입한 URL은 당신의 템플릿이 깨끗해도 계속 나타나며, 그 사건은 정확한 URL과 콘솔 줄을 첨부한 티켓에 속한다. 직원에게 안전하지 않은 콘텐츠를 허용하라고 하는 것은 그 브라우저 프로필이 방문하는 모든 사이트에서 검사가 꺼지기 때문에 이 선택지가 아니다.

브라우저 설정이 아니라 참조를 고친다

실패하는 흐름을 네트워크 로그를 남긴 채 다시 로드하고 의심되는 호스트로 패널을 거른다. 콘솔이 URL과 지시자를 지목하므로, 템플릿을 건드리기 전에 요청이 거부됐는지 업그레이드됐는지가 판가름 난다.

curl -sSI https://cdn.example.org/vendor.js | head -n 1
curl -sS -o /dev/null -w '%{http_code} %{url_effective}\n' -L https://cdn.example.org/vendor.js

보조 통제가 두 가지 있고, 둘 다 수정을 대신하지 않는다.upgrade-insecure-requests가 있는 Content-Security-Policy 헤더는 브라우저가 페이지를 떠나기 전에 http 하위 리소스 URL을 다시 쓰게 하며, 아직 찾지 못한 옛 참조를 잡아낸다. 다시 쓴 요청도 대상 호스트가 HTTPS를 제공하지 않으면 실패하므로, 헤더가 바꾸는 것은 브라우저가 시도할 URL뿐이다. HSTS는 호스트별로 동작하므로 자기 도메인의 정책은 그 헤더를 한 번도 보내지 않은 서드파티 CDN에는 아무 효과가 없다. 같은 URL이 파셜이나 번역 문자열, 데이터베이스 필드로도 들어올 수 있으므로, 템플릿을 고친 뒤 페이지 캐시를 비우고 렌더링된 HTML에서 옛 호스트 이름을 찾는다.

리디렉션도 따라가야 하는데, https://로 시작하는 사슬이 http:// 위치에 착지할 수 있고 브라우저는 마지막 도약을 판단하기 때문이다. 위 두 번째 명령의-L 형식이 그 유효 URL을 대신 출력해 준다. 마지막 도약이 순수 http라면, 고친 링크가 멀쩡해 보여도 참조는 여전히 틀렸다.

콘솔과 네트워크 패널로 페이지를 확인한다

캐시를 끄고 다시 로드해, 요청 시 내용을 불러오는 버튼도 포함해 흐름을 한 번 더 훑는다. 첫 페이지 로드에서는 지연 위젯, 지도 타일, 클릭 후에만 나타나는 플레이어 스크립트가 숨어 있다. 콘솔에 혼합 콘텐츠 항목이 없어야 하고, 손댄 모든 행이 https를 보여야 한다. 다음 배포 뒤에는 그 상태를 재현하기 어려우므로 티켓을 닫기 전에 콘솔과 요청 행을 기록한다.

리소스가 남의 것이라면 정확한 URL, 요청 유형, 콘솔 텍스트, 그리고 그것이 나타나는 페이지의 네 가지를 보낸다. 티켓에 붙이는 모든 것에서 세션 식별자를 가린다. 이미지가 깨졌다고만 적은 보고서는 소유자에게 검색할 단서를 주지 않는다.

수정이 요청을 만든 참조 안에 있는 동안 터널은 자기 일을 계속한다. 배포 뒤에 템플릿과 공급업체 번들에서 호스트 이름을 한 번 더 찾고, 콘솔에 차단된 혼합 콘텐츠 항목이 남지 않았을 때에만 사건을 닫는다.

관련 글