https 전환 후 남는 워드프레스 혼합 콘텐츠 해결하기
https로 옮긴 뒤에도 뜨는 워드프레스 혼합 콘텐츠 경고를 없애는 방법. 데이터베이스에 남은 http:// 주소를 찾아 안전하게 치환하고 .htaccess로 https를 강제합니다.
게시됨
인증서는 설치했고, 사이트는 https로 열리는데, 자물쇠 아이콘은 끝내 나타나지 않습니다. 그것이 혼합 콘텐츠입니다. 페이지 자체는 https로 왔지만 그 안의 무언가, 이미지나 스타일시트나 스크립트가 여전히 평문 http://로 요청되고 있는 것이죠. 이 글에서는 그런 주소를 찾아내고, 직렬화된 데이터를 망가뜨리지 않고 치환하고, siteurl과 home을 바로잡고, 서버에서 https를 강제해 문제가 되돌아오지 못하게 하는 과정을 따라갑니다.
혼합 콘텐츠란 실제로 무엇인가
인증서를 설치하면 연결이 암호화되는 방식이 바뀝니다. 페이지가 무엇을 요청하는지는 바뀌지 않습니다. 전환 전에 글이나 위젯, 테마 설정에 기록된 http://yoursite.com/wp-content/uploads/logo.png는 지금도 데이터베이스에 그대로 앉아 있고, 브라우저는 그것을 성실하게 안전하지 않은 연결로 요청합니다.
브라우저는 두 범주를 다르게 다루며, 이 구분은 얼마나 급한 일인지 판단할 때 중요합니다.
- 능동적 혼합 콘텐츠 — 스크립트, 스타일시트, iframe, XHR. 아예 차단됩니다. https 전환 뒤 어디에도 오류 메시지 없이 사이트가 깨져 보이는 이유가 이것입니다. 스타일시트가 조용히 거부된 것이죠.
- 수동적 혼합 콘텐츠 — 이미지, 오디오, 동영상. 보통은 로드되지만 자물쇠 표시가 강등되고 일부 브라우저는 「안전하지 않음」 표시를 보여줍니다.
둘 다 고칠 가치가 있습니다. 다만 실제로 망가지는 것은 앞쪽뿐입니다.
1단계: 아직 안전하지 않은 것이 무엇인지 찾기
브라우저에서 시작합니다. 페이지를 열고 개발자 도구를 열어 콘솔을 읽으세요. 차단되었거나 강등된 요청이 전체 URL과 함께 모두 나와 있습니다. 여기서 알 수 있는 것은 무엇이 안전하지 않은지까지이고, 그것이 어디에 저장되어 있는지는 알려주지 않습니다.
그건 데이터베이스를 직접 봐야 합니다. 내보낸 뒤 덤프를 검색하세요.
wp db export dump.sql
grep -o "http://example\.com[^\"']*" dump.sql | sort -u | head -50
WP-CLI를 쓸 수 없다면 mysqldump로도 같은 파일을 만들 수 있습니다.
mysqldump -u USER -p DBNAME > dump.sql
돌아온 고유 URL 목록을 읽습니다. 업로드 경로는 글 본문과 메타를 가리킵니다. 테마 자원 경로는 대개 옵션을 가리킵니다. 모르는 도메인이 보이면 외부 임베드이며, 이건 다른 방식으로 처리해야 합니다(아래에서 다룹니다).
2단계: siteurl과 home부터 고치기
siteurl과 home은 워드프레스가 생성하는 거의 모든 내부 주소를 만들 때 사용하는 두 옵션입니다. 둘 중 하나라도 http://로 남아 있으면, 데이터베이스의 나머지가 아무리 깨끗해도 워드프레스는 계속 안전하지 않은 주소를 내보냅니다.
확인합니다.
wp option get siteurl
wp option get home
sql로는 이렇게 합니다.
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');
이 둘은 직렬화된 배열이 아니라 평범한 문자열이므로, 여기서는 sql로 직접 업데이트해도 정말 안전합니다.
UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com')
WHERE option_name IN ('siteurl', 'home');
시간을 쏟기 전에 함정이 하나 있습니다. wp-config.php가 WP_HOME이나 WP_SITEURL을 정의하고 있다면, 이 상수들이 데이터베이스를 완전히 덮어써서 업데이트가 아무 일도 하지 않은 것처럼 보입니다.
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
거기서 값을 고치거나, 아예 지워서 데이터베이스가 결정하게 하세요. 이것부터 확인하십시오. 「바꿨는데 아무 변화가 없다」는 사례의 상당수가 이걸로 설명됩니다.
3단계: 치환은 직렬화를 이해해야 한다
데이터베이스의 나머지 전부가 진짜 위험이 숨은 곳이고, 그 위험은 대부분이 예상하는 것과 다릅니다. 위험한 건 치환이 URL을 놓치는 게 아닙니다. 텍스트 수준에서는 성공하면서 주변 데이터를 파괴하는 것입니다.
워드프레스는 배열과 객체를 PHP 직렬화 문자열로 저장하고, 그 형식은 포함된 모든 문자열의 바이트 길이를 기록합니다.
a:1:{s:4:"logo";s:38:"http://example.com/img/logo.png";}
원본 sql의 REPLACE로 http://를 https://로 바꾸면 텍스트는 1바이트 길어지지만 선언된 길이는 옛 값 그대로입니다. PHP는 그 길이를 읽고 그만큼 앞으로 이동한 뒤, 기대한 자리에서 종결자를 찾지 못해 배열 전체의 역직렬화를 거부합니다. 그러면 워드프레스는 그 설정이 애초에 저장된 적 없는 것처럼 동작하는 값을 테마나 플러그인에 넘깁니다.
증상은 오류가 아닙니다. 위젯이 사라지고, 사용자 정의하기 설정이 초기화되고, 페이지 빌더 레이아웃이 빈 화면으로 렌더링되는데, 관리 화면에는 무언가 잘못되었다는 표시가 전혀 없습니다. 백업 없이 그 상태에서 복구하는 일은 정말 고통스러우니 반드시 하나 만들어 두세요.
wp db export backup-before-https-replace.sql
그다음, 역직렬화하고 복원된 구조 안에서 치환한 뒤 길이를 바로잡아 다시 직렬화하는 도구를 쓰세요. 먼저 드라이런입니다.
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
보고서를 읽으세요. 모르는 테이블에서 수천 건이 잡힌다면, 확정하기 전에 멈추고 확인하십시오. 문제없다면 실제로 실행합니다.
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
--skip-columns=guid는 중요합니다. guid는 피드 리더를 위한 영구 식별자이지 살아 있는 주소가 아니며, 이를 다시 쓰면 구독자에게 보관된 글 전체가 새 글로 보일 수 있습니다. 실행 전에 어떤 열이 평범한 sql로 안전하고 어떤 열이 직렬화를 이해하는 처리가 필요한지 확인하고 싶다면, 워드프레스 검색 치환 sql 도구로 구문을 만들어 보세요.
명령줄을 쓸 수 없다면, 직렬화된 데이터를 처리한다고 명시한 마이그레이션 플러그인이 관리 화면에서 같은 디코딩 작업을 해줍니다. 그렇게 명시하지 않은 도구는 처리하지 못한다고 보시면 됩니다.
4단계: 서버에서 https 강제하기
데이터베이스를 정리하면 페이지가 안전하지 않은 자원을 요청하는 일은 멈춥니다. 하지만 방문자가 애초에 http://로 도착하는 것까지 막지는 못합니다. 그건 서버 수준의 리다이렉트이고, Apache에서는 .htaccess의 몫입니다.
# BEGIN Force HTTPS
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
# END Force HTTPS
위치에 대해 두 가지. 이것은 # BEGIN WordPress 마커 위에 두세요. 마커 안쪽은 누군가 고유주소를 저장할 때마다 버려집니다. 그리고 이 규칙은 mod_rewrite가 활성화되어 있고 가상 호스트가 재정의를 허용할 때(AllowOverride All, 최소한 FileInfo)만 동작합니다. nginx에서는 .htaccess가 완전히 무시되므로 대신 server 블록에 넣어야 합니다.
RedirectMatch로는 이 일을 할 수 없습니다. 요청 경로만 보고 매칭할 뿐, 현재 요청이 이미 안전한지 검사할 방법이 없어서 https 요청을 자기 자신으로 되돌려 보냅니다. 위의 조건부 RewriteRule이 알맞은 도구입니다.
리다이렉트 루프, 그리고 그 원인
그 규칙을 넣자마자 사이트가 끝없이 리다이렉트하기 시작한다면, TLS가 상류 어딘가에서, 로드 밸런서나 리버스 프록시나 CDN에서 종료되고 거기서 Apache로 평문 http가 전달되고 있는 것입니다. Apache는 안전하지 않은 요청을 보고 https로 리다이렉트하고, 프록시는 다시 http를 전달하고, 계속 돌게 됩니다.
대신 전달용 헤더를 검사하세요.
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
워드프레스 자체도 그런 구성에서는 같은 사각지대를 갖습니다. is_ssl()은 $_SERVER['HTTPS']를 읽는데 프록시는 이 값을 설정하지 않으므로 관리자 주소가 http://로 나옵니다. wp-config.php의 편집 중단 안내 줄 위에 다음을 추가하세요.
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
위험은 솔직히 말해 두겠습니다. X-Forwarded-Proto는 클라이언트가 보내는 헤더입니다. 이를 신뢰해도 되는 것은 내가 관리하는 프록시가 항상 값을 덮어쓸 때뿐입니다. 인터넷에서 직접 접근 가능한 서버에서는 위조될 수 있습니다.
데이터베이스 치환으로 해결되지 않는 것
- 파일에 직접 박힌 주소.
functions.php나 자식 테마 템플릿, 인라인 자원 경로에 쓰인 것은 데이터베이스에 없습니다. 테마 디렉터리는 따로 grep하세요. - 이스케이프된 JSON. 일부 빌더는 주소를
https:\/\/example.com형태로 저장합니다. 일반 형태로 검색하면 완전히 놓치므로, 이스케이프된 문자열에 대해 한 번 더 치환해야 할 수 있습니다. - 외부 리소스. http로만 제공되는 서드파티 스크립트나 임베드는 이쪽에서 고칠 수 없습니다. https 버전을 찾거나 걷어내세요.
- 캐시. 페이지 캐시, 오브젝트 캐시, CDN 사본은 치환이 제대로 끝난 뒤에도 옛 마크업을 계속 내보냅니다. 치환이 실패했다고 결론 내리기 전에 셋 다 비우세요.
한 가지 건너뛰어도 좋은 것이 있습니다. upgrade-insecure-requests Content Security Policy 헤더는 브라우저에서 안전하지 않은 요청을 다시 쓰게 해 경고를 잠재웁니다. 그러나 증상만 다룰 뿐 잘못된 주소는 데이터베이스에 그대로 남아, 다음 내보내기나 이전, 피드가 그것을 그대로 옮겨 나릅니다. 데이터를 먼저 고치고, 원한다면 그다음에 안전망으로 쓰세요.
그래도 해결되지 않는다면
각 단계가 끝날 때마다 siteurl과 home을 다시 확인하세요. 플러그인과 마이그레이션 도구가 뒤에서 값을 다시 쓰는 일이 있습니다. 콘솔이 지목한 안전하지 않은 주소를 덤프에서 찾을 수 없다면, 페이지 소스를 열어 거기서 검색해 보세요. 생성된 마크업에는 있는데 데이터베이스에는 없다면 그것은 PHP에서 만들어지는 것이며, 그것을 출력하는 테마나 플러그인이 고쳐야 할 대상입니다.
FAQ
질문
SSL 인증서를 설치했는데도 워드프레스 사이트에 혼합 콘텐츠 경고가 계속 뜨는 이유는 무엇인가요?
인증서는 연결이 암호화되는 방식만 바꿀 뿐, 페이지가 무엇을 요청하는지는 바꾸지 않습니다. 예전 http:// 주소는 글 본문, 위젯 옵션, 테마 설정 안에 그대로 데이터베이스에 남아 있습니다. 브라우저는 페이지를 https로 불러온 뒤 이미지나 스크립트가 평문 http로 요청되는 것을 발견하고, 겉보기에 안전한 페이지에서 혼합 콘텐츠를 보고합니다.
워드프레스 데이터베이스에 남아 있는 http:// 주소는 어떻게 찾나요?
브라우저로 페이지를 열고 개발자 콘솔을 읽으면 안전하지 않은 요청이 URL과 함께 모두 나열됩니다. 그다음 데이터베이스를 내보내고 덤프에서 http 접두사가 붙은 자기 도메인을 검색하세요. 테이블별 검색 결과 수를 세어 보면 문제가 글 본문에 있는지, wp_options에 있는지, 예상하지 못한 플러그인 테이블에 있는지 알 수 있습니다.
단순한 sql 검색 치환으로 혼합 콘텐츠를 고칠 수 있나요?
siteurl이나 home 같은 단일 값에만 가능합니다. 워드프레스가 직렬화된 배열로 저장하는 값, 즉 대부분의 옵션과 메타 값에는 내부 문자열마다 바이트 길이가 기록되어 있습니다. 원본 그대로 치환하면 텍스트만 바뀌고 길이는 옛 값으로 남아, 역직렬화에 실패하고 해당 설정은 조용히 기본값으로 돌아갑니다.
워드프레스에서 https를 강제하는 .htaccess 규칙은 무엇인가요?
https 변수를 검사하는 조건을 붙인 RewriteRule을, 고유주소를 저장해도 지워지지 않도록 BEGIN WordPress 마커 위에 둡니다. 프록시나 CDN 뒤에서는 안전한 요청에서도 https 변수가 off로 읽히므로, 대신 X-Forwarded-Proto 헤더를 검사해야 합니다. 그러지 않으면 규칙이 끝없이 리다이렉트합니다.
https를 강제했더니 리다이렉트 루프에 빠졌습니다. 왜 그런가요?
거의 확실히 TLS가 프록시나 CDN에서 종료되고, 거기서 Apache로는 평문 http가 전달되고 있습니다. 리라이트는 안전하지 않은 요청을 보고 https로 리다이렉트하고, 프록시는 또 http를 전달하고, 이것이 반복됩니다. 조건을 X-Forwarded-Proto 헤더로 바꾸고 wp-config.php에서 https 서버 변수를 설정하세요.
혼합 콘텐츠는 페이지 전체를 망가뜨리나요, 아니면 자물쇠 아이콘만의 문제인가요?
리소스에 따라 다릅니다. 스크립트, 스타일시트, iframe 같은 능동적 혼합 콘텐츠는 브라우저가 아예 차단하므로, 아무런 설명 없이 레이아웃이나 기능이 깨질 수 있습니다. 이미지나 동영상 같은 수동적 혼합 콘텐츠는 대개 그대로 로드되지만 자물쇠 표시가 강등되고 방문자에게 안전하지 않음 표시가 보일 수 있습니다. 둘 다 고칠 가치가 있습니다.