Skip to Content
WebJavaScriptWeb APIs19. 보안 — CSP·origin·권한

이번 편의 결과물: index.html에 Content-Security-Policy를 적용해 위반을 콘솔에서 확인하고, 알림 권한이 거부되면 화면 안내(토스트)로 대체합니다. · 다루는 개념: CSP 지시어, 권한 요청 UX, origin 격리

이 편에서 만드는 파일

notes-app/ ├── index.html (~ CSP 메타 태그 추가) └── src/ ├── render.js (~ showToast 추가) └── main.js (~ 알림 거부 시 대체 동작 연결)

개념 정리

Content Security Policy(CSP)

CSP는 브라우저에게 “이 페이지에서 허용할 리소스 출처”를 알려주는 정책입니다. <meta http-equiv="Content-Security-Policy">나 서버 응답 헤더로 지정합니다. 정책을 어기는 리소스는 로드가 막히고 콘솔에 위반 메시지가 뜹니다.

지시어의미
default-src다른 지시어가 없을 때 적용되는 기본값
script-src스크립트를 불러올 수 있는 출처. 'self'만 두면 인라인 스크립트·eval이 막힘
img-src이미지 출처. data:를 추가해야 첨부 이미지의 dataUrl이 보임
connect-srcfetch/XMLHttpRequest가 접속할 수 있는 출처

notes-app은 17편의 첨부 이미지(data: URL)와 json-server(http://localhost:3001) 접속이 있으므로 이 둘을 명시적으로 허용해야 합니다.

origin과 격리

origin은 프로토콜 + 호스트 + 포트의 조합입니다. http://localhost:5173http://127.0.0.1:5173은 호스트 문자열이 달라 서로 다른 origin입니다. 브라우저는 localStorage, IndexedDB, 쿠키를 origin 단위로 완전히 분리해서 저장합니다. 다른 origin의 페이지는 이 데이터를 읽을 방법이 없습니다.

비교 대상같은 origin 여부
http://localhost:5173 vs http://localhost:5173/notes/3같음(경로는 origin에 포함 안 됨)
http://localhost:5173 vs https://localhost:5173다름(프로토콜)
http://localhost:5173 vs http://localhost:3001다름(포트)
http://localhost:5173 vs http://127.0.0.1:5173다름(호스트 문자열)

권한 요청 UX

18편에서 Notification.requestPermission()을 썼습니다. 거부(denied) 상태에서는 다시 요청해도 팝업이 뜨지 않으므로, 앱은 권한이 없어도 핵심 기능(저장 완료 확인)을 사용자가 알 수 있는 대체 수단을 갖춰야 합니다.

실습

1. index.html에 CSP 메타 태그 추가

<!-- index.html의 head 안에 추가 --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; connect-src 'self' http://localhost:3001; script-src 'self'; style-src 'self' 'unsafe-inline';" />

2. 위반 테스트

개발자 도구 콘솔에서 아래 코드를 실행합니다.

document.body.setAttribute('onclick', "alert('csp-test')");

인라인 이벤트 핸들러는 script-src 'self' 정책에 걸려 실행되지 않고, 콘솔에 Refused to execute inline event handler 위반 메시지가 뜹니다.

3. src/render.js에 토스트 알림 함수 추가

// src/render.js 맨 아래에 추가 export function showToast(message) { const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = message; document.body.append(toast); setTimeout(() => toast.remove(), 2000); }

4. src/main.js에서 권한 거부 시 대체 동작 연결

18편에서 만든 enableNotifBtn, notifySaved를 아래처럼 바꿉니다.

// src/main.js (18편의 enableNotifBtn 리스너와 notifySaved 함수만 교체) import { showToast } from './render.js'; enableNotifBtn.addEventListener('click', async () => { const permission = await Notification.requestPermission(); if (permission === 'granted') { enableNotifBtn.textContent = '알림 켜짐'; } else { enableNotifBtn.textContent = '알림 거부됨'; showToast('알림 권한이 거부되어 화면 안내로 대체합니다'); } }); function notifySaved(note) { if (Notification.permission === 'granted') { new Notification('메모 저장됨', { body: note.title }); } else { showToast(`저장됨: ${note.title}`); } }

5. 실행

npm run dev

확인

  • 콘솔에 Content-Security-Policy 위반 로그 없이 앱이 정상 로드됩니다(이미지·json-server 접속 허용됨).
  • 2단계의 인라인 스크립트 테스트가 콘솔에 CSP 위반 메시지를 남기고 실행되지 않습니다.
  • 알림 권한을 거부한 뒤 메모를 저장하면, 화면 하단에 “저장됨: 제목” 토스트가 2초간 보입니다.
  • http://localhost:5173http://127.0.0.1:5173으로 각각 접속해 메모 목록이 서로 다르게(따로) 보이는 것을 확인합니다.

직접 해보기

  1. connect-src에서 http://localhost:3001을 빼고 08편의 fetch 동기화를 실행해 콘솔에 어떤 에러가 뜨는지 확인해봅니다.
  2. img-src에서 data:를 빼고 첨부 이미지가 있는 메모를 새로고침해 무엇이 달라지는지 확인해봅니다.

정답 보기

connect-src에서 http://localhost:3001을 빼면 fetchnet::ERR_BLOCKED_BY_CSP(콘솔 표시는 Refused to connect)로 실패합니다. img-src에서 data:를 빼면 첨부 이미지가 안 뜨고 Refused to load the image 'data:image/...' 위반 메시지가 콘솔에 뜹니다. 두 경우 모두 정책 문자열에 필요한 값을 다시 추가하면 해결됩니다.

자주 하는 실수

증상원인고치는 법
첨부 이미지가 갑자기 안 보임CSP img-srcdata: 누락img-src 'self' data:로 명시
json-server 동기화가 CSP 때문에 막힘connect-src에 서버 origin 누락connect-srchttp://localhost:3001 추가
인라인 style="..." 속성이 다 깨짐style-src에서 'unsafe-inline'인라인 스타일을 쓴다면 유지, 가능하면 CSS 파일로 옮겨 제거
다른 포트에서 접속했더니 메모가 하나도 없음정상 동작(origin이 달라 저장소가 분리됨)같은 origin(같은 호스트·포트)으로 접속

확인 문제

문제 14지선다
CSP의 img-src 지시어에 data:를 추가하지 않으면 notes-app에서 생기는 문제는?
문제 24지선다
http://localhost:5173과 http://127.0.0.1:5173에 대한 설명으로 옳은 것은?
문제 34지선다
Notification 권한이 denied인 사용자에게 notes-app이 취하는 대체 동작은?
문제 44지선다
script-src 'self' 정책이 있는 페이지에서 인라인 onclick 속성이 동작하지 않는 이유는?

참고 자료

Last updated on