Загружаем каталог…
Загружаем каталог…
보안 헤더는 처음부터 다 붙어 있었습니다. 문제는 그중 한 줄의 별표 하나였습니다. 한 줄 요약 이 사이트는 예전 사이트가 뚫린 적이 있어서 처음부터 CSP를 걸었습니다. 다만 script-src 의 'unsafe-inline' 은 남겼습니다 — 막으려면 엣지 캐시를 포기해야 했기 때문입니다. 그래서 실질적인 방어선은 "스크립트가 주입돼도 데이터가 밖으로 못 나가게" 하는 connect-src 였습니다. 그런데 거기에 https://*.supabase.co 가 적혀 있었습니다. 이 별표는 남의 Supabase 프로젝트까지 전부 허용 합니다. 빌드할 때 환경변수에서 우리 프로젝트 주소를 뽑아 하나로 좁혔고 , 같이 점검한 네 가지는 고칠 것이 없었습니다. 왜 처음부터 CSP였나 유진콤프레샤의 예전 사이트는 그누보드(PHP)였고, 뚫린 적이 있습니다. 그래서 새 사이트는 처음부터 공격면을 좁혔습니다. 지금 구조에는 PHP도, 서버에 올라가는 파일도, 직접 조립하는 SQL도 없습니다. 그 위에 브라우저가 강제하는 방어선을 한 겹 더 얹은 것이 보안 헤더입니다. Content-Security-Policy · X-Frame-Options · X-Content-Type-Options Referrer-Policy · Permissions-Policy 'unsafe-inline' 을 남긴 이유 — 트레이드오프 CSP의 정석은 인라인 스크립트를 막는 것입니다. 그런데 Next.js는 정적 페이지에 인라인 부트스트랩 스크립트를 넣습니다. 이걸 허용하면서 막으려면 요청마다 새 nonce 를 발급해야 하고, 그러려면 모든 페이지를 요청마다 새로 렌더 해야 합니다. 그러면 앞서 잡아 둔 엣지 캐시가 통째로 사라집니다. 설정 파일 주석에는 그 대가를 응답 0.08초 → 0.3초 이상 으로 적어 두었습니다. 회사 소개 사이트에서 매 요청을 서버가 다시 그리는 건 손님에게 드는 비용이 너무 컸습니다. 그래서 'unsafe-inline' 을 남기는 대신, 방어선을 다른 곳에 세웠습니다. 그래서 진짜 방어선은 "나가는 길" 스크립트 주입을 CSP로 완전히 막을 수 없다면, 주입된 스크립트가 할 수 있는 일 을 줄여야 합니다. 주입된 스크립트가 노리는 건 대개 데이터를 밖으로 보내는 것입니다. 그 출구가 connect-src (fetch·XHR가 갈 수 있는 곳)와 form-action (폼이 제출될 수 있는 곳)입니다. script-src 'self' 'unsafe-inline' ... (외부 도메인 스크립트 로드는 막힘) connect-src 'self' <허용한 곳만> ← 데이터가 나갈 수 있는 곳 form-action 'self' 외부 도메인 스크립트는 못 불러오고, 데이터가 나갈 수 있는 곳은 허용 목록뿐입니다. 설령 스크립트가 주입돼도 밖으로 빼내기가 어렵게 만드는 것이 이 구성의 핵심입니다. 별표 하나가 연 길 9월 11일 보안 점검에서 connect-src 와 img-src 를 다시 읽었습니다. connect-src 'self' https://*.supabase.co ... 이 와일드카드는 우리 Supabase 프로젝트만이 아니라 남의 Supabase 프로젝트까지 전부 허용합니다. 그리고 이 사이트의 관리자 로그인 토큰은 브라우저 저장소에 있습니다(Supabase 기본 동작). 그러면 이런 시나리오가 성립합니다. 어떤 경로로든 스크립트가 주입된다 ( 'unsafe-inline' 이 남아 있으니 CSP가 막지 못한다). 스크립트가 브라우저 저장소에서 관리자 토큰을 읽는다. 공격자가 만든 자기 Supabase 프로젝트 주소 로 그 토큰을 보낸다. 그 주소는 *.supabase.co 에 맞으니 CSP가 통과시킨다. CSP로 막으려던 바로 그 길이 열려 있던 셈입니다. 방어선을 "나가는 길"에 세워 놓고, 그 길에 별표를 적어 둔 겁니다. 고친 방법 — 빌드할 때 출처를 하나로 빌드할 때 NEXT_PUBLIC_SUPABASE_URL 에서 출처를 뽑아 CSP에 그대로 박습니다(커밋 420c509 ). 이제 connect-src 와 img-src 에는 우리 프로젝트 주소 하나만 들어갑니다. 환경변수가 없을 때(로컬에서 .env 없이 켤 때)는 예전 와일드카드로 되돌립니다. 화면이 통째로 깨지는 것보다는 낫고, 배포 환경에는 항상 값이 있기 때문 입니다. 같이 확인한 것 — 고칠 것 없음 와일드카드 하나를 찾은 김에, 같은 종류의 구멍이 있을 만한 자리를 다 확인했습니다. 네 가지 모두 고칠 것이 없었습니다. 없었다는 것도 확인해야 아는 일이라 같이 적어 둡니다. 점검 결과 dangerouslySetInnerHTML 3곳 전부 검색엔진용 JSON-LD이고, < > & 를 이스케이프한다. 관리자가 DB에 무엇을 적어도 </script> 로 빠져나갈 수 없다 DB 행 단위 보안(RLS) 실측 — 문의가 17건 있는데 공개 키(anon)로 조회하면 0건. 고객 개인정보는 막혀 있다 서버 전용 비밀키 SUPABASE_SERVICE_ROLE_KEY 는 NEXT_PUBLIC_ 없이 서버 전용이고, 한 파일에서만 쓴다 민감한 API 개인정보 파기( /api/retention )는 Cron 비밀값을, 번역( /api/translate )은 관리자 토큰을 검증한다 RLS는 설정 화면을 보는 것으로 끝내지 않고, 실제로 공개 키로 조회해서 0건이 나오는 것까지 봤습니다. 같은 원칙을 다음에도 같은 날 오류 트래킹(Sentry)을 붙일 때, connect-src 에 Sentry 주소를 넣어야 했습니다. 이때도 *.ingest.sentry.io 와일드카드를 쓰지 않고 DSN에서 뽑은 호스트 하나만 넣었습니다. 설정 파일에 이 이유를 주석으로 남겨, 다음에 출처를 추가하는 사람이 같은 실수를 하지 않게 했습니다. 마무리 — 무엇을 배웠나 첫째, 막지 못하는 것이 있으면 방어선을 옮깁니다. 'unsafe-inline' 을 남긴 대신, 데이터가 나가는 길을 좁히는 것이 실질적인 방어였습니다. 둘째, 방어선의 가치는 가장 넓은 구멍이 정합니다. connect-src 를 아무리 촘촘히 적어도, 별표 하나가 있으면 그 줄 전체가 무력해집니다. 셋째, "없다"도 확인해서 적습니다. RLS가 막고 있다는 건 설정이 아니라 실제 조회 결과 0건으로 확인했습니다. 이 글에서 남는 것 Next.js 정적 페이지에서 nonce를 쓰면 엣지 캐시를 잃는다. 'unsafe-inline' 을 남긴다면 방어선은 connect-src · form-action 으로 옮긴다. *.supabase.co 같은 서비스 와일드카드는 남의 프로젝트까지 허용한다. 토큰을 공격자 프로젝트로 보낼 수 있다. 출처는 빌드 때 환경변수에서 뽑아 하나로 박는다. 로컬 폴백은 두되 배포에는 항상 값이 있게 한다. 와일드카드를 하나 찾으면 같은 종류(JSON-LD, RLS, 서버 키, API 인증)를 같이 점검한다. 새 출처를 추가할 때도 같은 원칙 — 호스트 하나만.
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
막으려던 길이 열려 있었다 — CSP 와일드카드 하나가 열어 둔 토큰 유출 경로. 보안 헤더는 처음부터 다 붙어 있었습니다. 문제는 그중 한 줄의 별표 하나였습니다. 한 줄 요약 이 사이트는 예전 사이트가 뚫린 적이 있어서 처음부터 CSP를 걸었습니다. 다만 script-src 의 'unsafe-inline' 은 남겼습니다 — 막으려면 엣지 캐시를 포기해야 했기 때문입니다. 그래서 실질적인 방어선은 "스크립트가 주입돼도 데이터가 밖으로 못 나가게" 하는 connect-src 였습니다. 그런데 거기에 https://*.supabase.co 가 적혀 있었습니다. 이 별표는 남의 Supabase 프로젝트까지 전부 허용 합니다. 빌드할 때 환경변수에서 우리 프로젝트 주소를 뽑아 하나로 좁혔고 , 같이 점검한 네…
Открыть источник