브라우저가 문제 삼는 것
페이지는 HTTPS로 왔는데, 그 페이지 안의 무언가가 평문 http://로 리소스를 요청했습니다. 그 리소스는 네트워크 경로에 있는 누구든 읽거나 바꿔치기할 수 있고, 그러면 HTTPS 페이지인 의미가 없어집니다. 그래서 브라우저는 요청을 차단하거나 HTTPS로 다시 시도합니다. “HTTPS인데 일부가 차단된다”는 현상의 정체가 이것입니다.
크롬 콘솔에서 차단된 요청은 이렇게 보입니다.
Mixed Content: The page at 'https://example.com/' was loaded over HTTPS, but requested an
insecure script 'http://cdn.example.net/widget.js'. This request has been blocked; the
content must be served over HTTPS.
업그레이드된 요청은 대신 This request was automatically upgraded to HTTPS라고 나옵니다.
둘 중 무엇이 일어나는지는 리소스 종류에 달려 있습니다.
차단되는 것과 업그레이드되는 것
W3C Mixed Content 명세는 리소스를 두 무리로 나눕니다.
차단 대상(blockable)(active mixed content라고도 부릅니다): 스크립트, 스타일시트, iframe, 웹폰트, fetch()와 XHR, WebSocket, worker, 그리고 사실상 두 번째 무리에 속하지 않는 모든 것입니다. 이것들은 페이지를 읽거나 고쳐 쓸 수 있으므로, 변조된 사본은 공격자가 내 사이트에서 코드를 실행하는 것과 같습니다. 브라우저는 이것들을 차단합니다. 아무것도 로드되지 않고, 거기에 기대던 것이 깨집니다. 스타일시트 없는 레이아웃, 스크립트 없이 죽은 버튼, iframe이 있던 빈 칸 같은 식입니다.
업그레이드 대상(upgradeable)(예전 이름은 passive 또는 display content): <img>, <audio>, <video>로 불러오는 이미지, 오디오, 비디오입니다. 변조된 이미지도 나쁘지만 피해가 제한적입니다. 현재 크롬과 파이어폭스는 이것들을 더 이상 HTTP로 받지 않고, URL을 조용히 https://로 바꿔 시도합니다. 호스트가 같은 파일을 HTTPS로 제공하면 아무 일도 없었던 것처럼 보입니다. 제공하지 않으면 이미지가 그냥 빠지고, 콘솔 메시지 외에는 아무 표시도 없습니다. 예전 브라우저는 HTTP로 받고 주소창의 보안 표시만 낮췄습니다.
이 구분이 흔한 헷갈림을 설명해 줍니다. “이미지는 나오는데 슬라이더가 안 돌아간다.” 이미지는 업그레이드됐고 마침 호스트가 HTTPS를 지원했던 것이고, 슬라이더 스크립트는 차단된 것입니다.
방문자라면: 할 수 있는 것
Mixed Content 때문에 깨진 사이트를 쓰려는 방문자라면, 고칠 사람은 사이트 운영자입니다. 어떤 리소스가 실패하는지(콘솔 메시지에 이름이 나옵니다) 알려 줄 수는 있습니다.
크롬은 사이트 하나에 대해 이 보호를 끌 수 있게 해 둡니다. 주소창 왼쪽의 사이트 정보 아이콘을 누르고 사이트 설정에서 안전하지 않은 콘텐츠를 허용으로 바꾼 뒤 새로고침합니다. 다른 브라우저에도 비슷한 사이트별 설정이 있습니다.
믿을 수 있는 사이트를 믿을 수 있는 네트워크에서 쓸 때만, 그리고 잠깐만 하세요. HTTPS 페이지를, HTTP로 받아 온 스크립트를 중간의 누구든 바꿔치기할 수 있는 상태로 되돌리는 일입니다. 이 차단이 막으려는 공격이 정확히 그것입니다. 다른 방문자의 페이지를 고쳐 주는 설정은 없습니다.
사이트 운영자라면: http:// 참조를 모두 찾기
원본을 고치세요. 브라우저의 자동 업그레이드에 기대 덮어 두지 마세요. 차단 대상은 자동 업그레이드되지 않고, 예전 브라우저는 아무것도 업그레이드하지 않습니다.
개발자 도구로
문제 페이지를 개발자 도구(F12)를 미리 연 상태에서 새로고침합니다.
- Console: Mixed Content 요청마다 메시지가 하나씩, 페이지 URL과 리소스 URL을 함께 남깁니다. 콘솔 필터에
Mixed Content를 입력하면 그것만 보입니다. - Network: 필터 칸에
mixed-content:all을 입력하면 페이지의 모든 혼합 요청이,mixed-content:displayed를 입력하면 실제로 로드된 것만 나옵니다.
사이트의 주요 템플릿을 돌아보세요. 홈, 게시글, 상품 페이지, 결제, 로그인 후 화면. Mixed Content는 사이트 단위가 아니라 대개 템플릿 단위로 생깁니다.
명령줄로
서버에서 렌더링한 HTML이라면 거친 grep으로도 대부분 찾습니다.
curl -s https://example.com/some-page | grep -oE '(src|href|action)="http://[^"]+"' | sort -u
자바스크립트가 실행 중에 만드는 URL과 CSS 파일 안의 url(http://...)는 놓칩니다. 스타일시트는 파일마다 받아서 같은 방법으로 grep하세요.
사이트 전체를, 실제 브라우저에서
report-only CSP를 걸면 방문자의 브라우저가 아무것도 막지 않고 HTTPS가 아닌 모든 로드를 보고해 줍니다.
Content-Security-Policy-Report-Only: default-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; report-uri /csp-report
https:(또는 data:·blob:)가 아닌 요청마다 페이지와 막혔을 URL을 담은 JSON 보고서가 /csp-report로 옵니다. 며칠 켜 두면 아무도 눌러 보지 않던 페이지까지 찾게 됩니다. report-uri는 공식적으로 report-to로 대체됐지만, 브라우저는 여전히 지원하고 설정이 더 간단합니다.
원인별 해결
템플릿과 코드에 하드코딩된 http://
가장 단순한 경우입니다. 테마, 템플릿, 스크립트에 http://가 적혀 있습니다. https://로 바꾸세요. 바꾸기 전에 각 URL의 https:// 버전을 브라우저로 열어서 그 호스트가 실제로 HTTPS로 파일을 주는지 확인하세요. 프로토콜 상대 URL(//cdn.example.net/x.js)도 동작하지만, 이제 무언가를 HTTP로 제공할 이유가 없으니 그냥 https://라고 쓰면 됩니다.
데이터베이스에 저장된 http:// URL
CMS라면 Mixed Content는 코드보다 콘텐츠에 더 많이 있습니다. 옛 게시글 속 이미지 URL, 위젯 설정, 메뉴 링크가 HTTPS 전환 전의 http://example.com/... 그대로 저장돼 있습니다.
워드프레스라면 먼저 설정 → 일반 → 워드프레스 주소와 사이트 주소를 https://로 바꿉니다. 그다음 WP-CLI로 저장된 URL을 바꿉니다.
wp db export before-https.sql
wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid --dry-run
wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid
dry run은 테이블마다 몇 건을 바꿀지 보여 줍니다. SQL의 REPLACE()를 직접 쓰지 말고 WP-CLI를 쓰세요. 워드프레스는 많은 설정을 직렬화된 PHP로 저장하는데, 문자열마다 길이가 붙어 있습니다(s:18:"http://example.com"). http를 https로 바꾸면 한 글자가 늘어 저장된 길이와 맞지 않게 되고, PHP는 그 값을 역직렬화하지 못합니다. 위젯과 테마 옵션이 조용히 초기화됩니다. wp search-replace는 길이까지 고쳐 줍니다. --skip-columns=guid는 게시글 GUID를 건드리지 않게 하는 옵션이고, 워드프레스는 GUID를 바꾸지 말라고 권합니다.
다른 CMS도 도구만 다를 뿐 같은 문제가 있습니다. 원칙은 같습니다. 백업하고, 저장 형식을 이해하는 도구로 바꾸고, 결과를 확인합니다.
앱이 자기가 HTTP에서 돈다고 생각함
코드에서 찾을 수 없는 Mixed Content를 만드는 경우입니다. HTTPS가 로드밸런서, CDN, 리버스 프록시에서 끝나고, 요청은 평문 HTTP로 애플리케이션에 도착합니다. 애플리케이션은 자기 연결만 보고 스킴이 http라고 판단해, 자산·canonical 태그·리다이렉트 등 모든 절대 URL을 http://로 만듭니다.
해결은 원래 스킴을 전달하고 애플리케이션이 그 값을 믿게 하는 것입니다. 앱 앞에 nginx가 있다면:
proxy_set_header X-Forwarded-Proto $scheme;
그다음 애플리케이션 쪽에서:
- Django:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") - Express:
app.set("trust proxy", 1)(앱 앞에 있는 프록시 수) - 프록시 뒤의 워드프레스:
wp-config.php의 “편집 중지” 주석 위에
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
이 헤더는 내 프록시가 보낸 것만 믿어야 합니다. 애플리케이션에 직접 접속할 수 있다면 누구나 X-Forwarded-Proto: https를 보낼 수 있습니다.
프록시가 Flexible 모드의 Cloudflare라면, 엣지는 설계상 오리진과 HTTP로 통신합니다. Full (strict)로 바꾸면 증상이 아니라 원인을 고칩니다. 차이는 Cloudflare SSL 모드 가이드에 정리돼 있습니다.
HTTPS가 없는 외부 호스트
80번 포트로만 응답하는 오래된 위젯, 광고 서버, 분석 스크립트, 이미지 호스트입니다. 내 쪽 설정으로는 해결할 수 없습니다. 업그레이드된 요청은 도착할 곳이 없고, 차단 대상은 어차피 차단됩니다.
선택지는 이 순서입니다. 업체가 지금 쓰는 HTTPS 주소를 찾거나(대부분 오래전에 옮겼습니다), 라이선스가 허락하면 파일을 받아 내 서버에서 HTTPS로 제공하거나, 빼 버리는 것입니다. 그 호스트가 HTTPS로 응답하기는 하는지, 인증서는 유효한지는 SSL 점검으로 확인할 수 있습니다.
http://로 제출되는 폼
HTTPS 페이지에 있는 폼의 action이 http://...라면 이것도 Mixed Content입니다. 크롬은 제출 전에 사용자에게 경고하고, 계속하면 데이터가 평문으로 전송됩니다. action을 https://나 상대 경로로 바꾸세요.
안전망으로서의 upgrade-insecure-requests
알고 있는 참조를 고친 뒤에는, 놓친 것을 브라우저가 업그레이드하도록 지시할 수 있습니다.
Content-Security-Policy: upgrade-insecure-requests
헤더를 설정할 수 없다면 페이지 안에:
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
이 지시어가 있으면 브라우저는 페이지의 모든 http:// 서브리소스 URL을 요청 전에 https://로 바꿉니다. 차단 대상도, 외부 호스트도 포함입니다. 내 사이트로 이동하는 링크도 업그레이드합니다. 다른 사이트로 가는 링크는 업그레이드하지 않습니다.
한계가 두 가지 있습니다. 모든 호스트가 같은 내용을 HTTPS로도 제공한다고 가정하므로, 그렇지 않은 호스트에서는 차단 대신 요청 실패가 나서 페이지는 여전히 깨집니다. 그리고 이 헤더를 보낸 페이지에만 적용되므로 HSTS를 대신하지 못합니다. HSTS는 방문자가 직접 입력한 첫 요청까지 포함해 사이트 전체에서 브라우저가 HTTPS를 쓰게 만듭니다. 둘의 관계는 HTTP 보안 헤더 가이드를 보세요.
block-all-mixed-content도 보일 수 있습니다. 폐기된 지시어이고, 현재 브라우저는 이미 그것이 막을 것을 막고 있어서 쓸모가 없습니다.
고쳐졌는지 확인하기
- 주요 템플릿마다 개발자 도구를 연 채 새로고침해서, 콘솔에 Mixed Content 메시지가 없고 Network의
mixed-content:all에 아무것도 나오지 않는지 확인합니다. - HTTP 점검으로 페이지 자체가 HTTPS로 제공되는지, HTTP가 그쪽으로 리다이렉트되는지, 어떤
Content-Security-Policy를 보내는지 확인합니다. - report-only CSP는 조금 더 켜 두세요. 눌러 보지 못한 페이지까지 잡아 줍니다.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.