CSP 쓰는 사이트에 구글 태그 붙이기

Content-Security-Policy 를 걸어둔 사이트에 태그 관리자를 붙였다. 코드는 멀쩡히 들어갔는데 아무 일도 안 일어났다.

이 글은 그걸 고치는 순서다. 한 번에 다 열어야 한다. 하나씩 열면 배포를 세 번 하게 된다. 내가 그랬다.

막혔다는 걸 알아채는 법

CSP 차단은 HTML 에 흔적이 없다. 스니펫도 들어 있고 스크립트 태그도 DOM 에 있다.

[...document.scripts].filter(s => s.src.includes('googletagmanager')).length
// 1  ← 태그는 있다

그런데 받아온 게 없다.

performance.getEntriesByType('resource')
  .filter(e => e.name.includes('googletagmanager'))
  .map(e => ({ size: e.transferSize, dur: e.duration }))
// [{ size: 0, dur: 0 }]

transferSize 가 0 이면 차단이다. 이유는 콘솔에 찍혀 있다.

Loading the script 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX'
violates the following Content Security Policy directive: "script-src 'self' ..."

콘솔을 안 보면 못 찾는다. 네트워크 탭에서도 요청이 잡히긴 하는데 실패로 표시되지 않기도 한다.

열어야 할 지시어 넷

한 도메인이 여러 지시어에 걸린다. 여기가 핵심이다.

| 지시어 | 무엇 때문에 |

|—|—|

| script-src | gtm.js, gtag/js 로드 |

| connect-src | 수집 요청. *.google-analytics.com, *.analytics.google.com |

| img-src | 픽셀 방식 수집 폴백 |

| frame-src | noscript iframe(ns.html) 과 태그 관리자 미리보기 |

설정

Next.js 기준이다. 다른 프레임워크도 헤더 값은 같다.

const csp = [
  "default-src 'self'",
  `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""} https://www.googletagmanager.com`,
  "style-src 'self' 'unsafe-inline'",
  "img-src 'self' data: blob: https://www.googletagmanager.com https://*.google-analytics.com",
  "font-src 'self' data:",
  "connect-src 'self' https://www.googletagmanager.com https://*.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com",
  "frame-src 'self' https://www.googletagmanager.com https://tagassistant.google.com",
  "frame-ancestors 'none'",
  "base-uri 'self'",
  "form-action 'self'",
].join("; ");

tagassistant.google.com 은 미리보기 모드가 열리는 곳이다. 이걸 빼면 태그를 확인할 방법이 없어진다.

'unsafe-eval' 은 안 열어도 된다. 태그 관리자는 그것 없이 돈다.

광고도 같이 쓴다면

애드센스가 붙어 있으면 무효 트래픽 판정용 도메인이 하나 더 필요하다. 이것도 네 지시어에 다 걸린다.

https://*.adtrafficquality.google

script-src, connect-src, img-src, frame-src 전부에 넣는다.

확인

배포하고 새 탭에서 연다.

new Promise(r => setTimeout(() => r(JSON.stringify({
  container: Object.keys(window.google_tag_manager||{}).includes('GTM-XXXXXXX'),
  gtm_start: (window.dataLayer||[]).filter(o=>o&&o['gtm.start']).length,
  scripts: [...document.scripts].filter(s=>s.src.includes('gtm.js')).length,
}, null, 1)), 5000))

container: true 가 나오면 통과다. 콘솔 오류도 0 건이어야 한다.

여기서 시간을 날렸다

같은 탭에서 새로고침하며 확인했다. 콘솔에 이전 로드의 오류가 그대로 남아 있어서 고친 뒤에도 안 고쳐진 줄 알았다.

[error] ... violates ... "script-src 'self' ... "                    ← 옛 CSP
[error] ... violates ... "connect-src 'self' ... googletagmanager"   ← 새 CSP

두 오류의 CSP 내용이 다르다. 이걸 알아채기 전까지 헤맸다. 탭을 새로 열고 보면 이런 착시가 없다.

하나씩 열면 이렇게 된다

내가 실제로 겪은 순서다.

1차  connect-src, frame-src 열었다   →  script-src 에서 sodar2.js 가 막힘
2차  script-src 열었다               →  img-src 에서 수집 픽셀이 막힘
3차  img-src 열었다                  →  통과

배포가 세 번 나갔다. 한 번 고치고 콘솔을 본다. 오류가 남아 있으면 또 고친다. 오류가 0 이 될 때까지 반복한다. 첫 오류가 사라졌다고 끝난 게 아니다.

앞의 지시어 표대로 처음부터 넷을 다 열면 한 번에 끝난다.

덤으로 발견한 것

콘솔을 보다가 이것도 나왔다.

Loading the script 'https://static.cloudflareinsights.com/beacon.min.js/...'
violates the following Content Security Policy directive: "script-src ..."

Cloudflare Web Analytics 비콘이 몇 달째 막혀 있었다. 태그 작업과 무관하게 원래 그랬다.

CSP 를 처음 걸 때 콘솔을 안 보면 이렇게 조용히 죽는 게 생긴다. 사이트는 멀쩡히 보이니까 모른다. CSP 를 새로 도입했다면 한 번 훑어볼 만하다.

안 되면

container: false 인데 콘솔은 깨끗하다. CSP 문제가 아니다. 컨테이너에 태그가 게시됐는지 본다.

curl -s "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX" | grep -c 'G-XXXXXXXXXX'

헤더가 안 바뀐다. 배포는 됐는데 CDN 이 옛 응답을 주고 있을 수 있다. 캐시 우회 쿼리를 붙여 여러 번 찍어본다.

for i in 1 2 3 4 5; do curl -sI "https://내도메인/?cb=$RANDOM$i" | grep -io "script-src[^;]*"; done

남은 것

CSP 위반을 서버로 모아 보는 report-to 지시어가 있는데 아직 안 붙였다. 붙여뒀으면 Cloudflare 비콘이 막힌 걸 몇 달 전에 알았을 것이다.

← 블로그 목록