크롬 확장 보안 강화: CSP와 최소 권한 원칙

크롬 확장은 강력한 권한을 가지므로 보안이 중요합니다. CSP로 인라인 스크립트와 동적 코드 실행을 차단하고, 불필요한 권한과 리소스 노출을 제거하여 공격 표면을 최소화했습니다.

1. 왜 크롬 확장 보안이 중요한가?

확장 프로그램의 특수한 위치

크롬 확장은 일반 웹사이트보다 더 많은 권한을 가집니다:

  • 브라우저 탭과 북마크에 접근
  • 로컬 스토리지에 데이터 저장
  • 다른 웹사이트의 콘텐츠 읽기/수정 가능
  • 시스템 알림 표시

이 강력한 권한은 보안 취약점이 발생했을 때 피해가 더 크다는 의미이기도 합니다.

실제 발생 가능한 위협

1. XSS (Cross-Site Scripting)

악의적인 데이터가 DOM에 삽입되어 스크립트가 실행되는 공격입니다.

2. 코드 인젝션

문자열을 코드로 평가하는 함수들을 통해 임의 코드가 실행되는 공격입니다. CSP는 이런 패턴들을 차단합니다.

3. 리소스 탈취

<!-- 외부에서 확장의 내부 페이지에 접근 시도 -->
<iframe src="chrome-extension://확장ID/manager/manager.html"></iframe>

2. Content Security Policy (CSP)

CSP란?

Content Security Policy는 브라우저에게 "어떤 리소스를 신뢰할지" 알려주는 보안 정책입니다. 허용되지 않은 스크립트, 스타일, 이미지 등의 로드를 차단합니다.

Manifest V3의 기본 CSP

Manifest V3에서는 기본적으로 엄격한 CSP가 적용됩니다:

script-src 'self';
object-src 'self';

하지만 명시적으로 선언하여 더 강화할 수 있습니다.

CSP 설정하기

// manifest.json
{
  "manifest_version": 3,
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'"
  }
}

CSP 지시어 설명

지시어 설명 예시
script-src JavaScript 소스 제한 'self' = 확장 내부만
object-src <object>, <embed> 등 플러그인 제한 'none' = 완전 차단
style-src CSS 소스 제한 'self' 'unsafe-inline'
img-src 이미지 소스 제한 'self' https:
connect-src fetch, XHR 대상 제한 'self' https://api.example.com

각 값의 의미

의미
'self' 같은 출처(확장 자체)만 허용
'none' 완전 차단
'unsafe-inline' 인라인 코드 허용 (지양)
'unsafe-eval' 동적 코드 평가 허용 (지양)
https: HTTPS 출처만 허용
https://example.com 특정 도메인만 허용

3. 실전 CSP 설정

3.1 가장 엄격한 설정 (권장)

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'none'"
  }
}

이 설정의 효과:

  • ✅ 확장 내부 스크립트만 실행 가능
  • <script> 태그의 인라인 코드 차단
  • ✅ 동적 코드 실행 패턴 차단
  • ✅ 플러그인(Flash 등) 완전 차단

3.2 인라인 스타일이 필요한 경우

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'"
  }
}

'unsafe-inline' for styles가 필요한 이유:

  • JavaScript에서 element.style.property = value 사용
  • 동적으로 스타일 변경 (토스트 애니메이션 등)
// 이런 코드가 동작하려면 style-src 'unsafe-inline' 필요
toast.style.opacity = '0';
toast.style.transform = 'translateY(10px)';

3.3 외부 API 호출이 필요한 경우

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'none'; connect-src 'self' https://api.example.com"
  }
}

주의: 외부 도메인을 추가하면 그만큼 공격 표면이 넓어집니다.

3.4 외부 이미지 로드가 필요한 경우

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'none'; img-src 'self' https: data:"
  }
}
  • https: - HTTPS 이미지만 허용
  • data: - Data URL 이미지 허용 (base64 인코딩)

4. CSP가 차단하는 것들

4.1 인라인 스크립트 (차단됨)

<!-- ❌ CSP에 의해 차단됨 -->
<button onclick="handleClick()">Click</button>

<script>
  console.log('inline script');
</script>

해결책: 외부 파일로 분리

<!-- ✅ 안전한 방법 -->
<button id="myButton">Click</button>
<script src="script.js"></script>
// script.js
document.getElementById('myButton').addEventListener('click', handleClick);

4.2 동적 코드 실행 (차단됨)

CSP의 script-src 'self'는 문자열을 코드로 평가하는 모든 패턴을 차단합니다. 문자열을 인자로 받는 setTimeout/setInterval, 동적 함수 생성 등이 이에 해당합니다.

해결책: 직접 함수 호출

// ✅ 안전한 방법
console.log("hello");
const add = () => 1 + 1;
setTimeout(() => console.log("hello"), 1000);  // 함수 버전

4.3 외부 스크립트 (차단됨)

<!-- ❌ CSP에 의해 차단됨 -->
<script src="https://cdn.example.com/library.js"></script>

해결책: 라이브러리를 확장에 번들링

<!-- ✅ 확장 내부에 파일 포함 -->
<script src="lib/library.js"></script>

5. 최소 권한 원칙

필요한 권한만 요청하기

// ❌ 과도한 권한
{
  "permissions": [
    "tabs",
    "bookmarks",
    "history",
    "storage",
    "cookies",
    "webRequest",
    "<all_urls>"
  ]
}

// ✅ 필요한 권한만
{
  "permissions": [
    "tabs",
    "bookmarks",
    "storage",
    "alarms"
  ]
}

권한별 위험도

권한 위험도 설명
storage 낮음 확장 전용 스토리지
alarms 낮음 타이머 기능
tabs 중간 탭 URL 읽기 가능
bookmarks 중간 북마크 읽기/수정
history 높음 방문 기록 전체 접근
cookies 높음 모든 쿠키 접근
webRequest 매우 높음 네트워크 요청 가로채기
<all_urls> 매우 높음 모든 웹사이트 접근

Optional Permissions 활용

필요할 때만 권한을 요청:

// manifest.json
{
  "permissions": ["storage", "alarms"],
  "optional_permissions": ["tabs", "bookmarks"]
}
// 필요할 때 권한 요청
async function requestBookmarkPermission() {
  const granted = await chrome.permissions.request({
    permissions: ['bookmarks']
  });

  if (granted) {
    console.log('Bookmark permission granted');
    // 북마크 기능 활성화
  }
}

6. web_accessible_resources 최소화

web_accessible_resources란?

web_accessible_resources웹 페이지에서 접근 가능한 확장 리소스를 정의합니다.

// manifest.json
{
  "web_accessible_resources": [
    {
      "resources": ["images/icon.png", "content.css"],
      "matches": ["<all_urls>"]
    }
  ]
}

보안 위험

이 설정이 있으면 어떤 웹사이트에서든 확장의 리소스에 접근할 수 있습니다:

<!-- 악의적인 웹사이트 -->
<img src="chrome-extension://EXTENSION_ID/images/icon.png">
<iframe src="chrome-extension://EXTENSION_ID/page.html"></iframe>

위험:

  1. 확장 설치 여부 감지 (핑거프린팅)
  2. 내부 페이지 iframe으로 삽입
  3. 리소스 URL을 통한 확장 ID 노출

해결책: 불필요한 리소스 제거

// ❌ Before: 불필요한 페이지 노출
{
  "web_accessible_resources": [
    {
      "resources": ["manager/manager.html", "onboarding/onboarding.html"],
      "matches": ["<all_urls>"]
    }
  ]
}

// ✅ After: 완전히 제거 (필요 없으면)
{
  // web_accessible_resources 키 자체를 삭제
}

정말 필요한 경우만 제한적으로

Content Script에서 확장 리소스를 사용해야 하는 경우:

{
  "web_accessible_resources": [
    {
      "resources": ["content/injected.css"],
      "matches": ["https://specific-site.com/*"],  // 특정 사이트만
      "use_dynamic_url": true  // 동적 URL 사용 (ID 숨김)
    }
  ]
}

7. 실제 적용 예시

Before: 보안 취약한 manifest.json

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "permissions": [
    "tabs",
    "bookmarks",
    "storage",
    "contextMenus",
    "history",      // ← 불필요
    "cookies",      // ← 불필요
    "<all_urls>"    // ← 과도함
  ],
  "web_accessible_resources": [
    {
      "resources": ["*"],           // ← 모든 리소스 노출!
      "matches": ["<all_urls>"]     // ← 모든 사이트에서 접근 가능
    }
  ]
  // CSP 미설정 → 기본값 사용
}

After: 보안 강화된 manifest.json

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "permissions": [
    "tabs",
    "bookmarks",
    "storage",
    "contextMenus",
    "alarms"
  ],
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'"
  }
  // web_accessible_resources 제거됨
}

변경 사항:

  1. 불필요한 권한 제거 (history, cookies, <all_urls>)
  2. CSP 명시적 설정
  3. web_accessible_resources 완전 제거

8. 코드 레벨 보안 실천

8.1 DOM 조작 시 textContent 사용

사용자 입력이나 외부 데이터를 DOM에 삽입할 때는 항상 textContent를 사용해야 합니다. 문자열 기반 HTML 삽입 메서드는 XSS 취약점을 만듭니다.

// ✅ 안전한 방법
element.textContent = userInput;

8.2 URL 검증

// ✅ 안전: 프로토콜 검증
function openUrl(url) {
  try {
    const parsed = new URL(url);
    if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
      window.open(url);
    }
  } catch (e) {
    console.error('Invalid URL');
  }
}

8.3 입력값 길이 제한

const MAX_URL_LENGTH = 2048;
const MAX_TITLE_LENGTH = 500;

function validateInput(url, title) {
  if (url.length > MAX_URL_LENGTH) {
    return { valid: false, error: 'URL too long' };
  }
  if (title.length > MAX_TITLE_LENGTH) {
    return { valid: false, error: 'Title too long' };
  }
  return { valid: true };
}

8.4 SVG 생성 시 DOM API 사용

SVG를 생성할 때는 문자열 기반이 아닌 DOM API를 사용해야 합니다:

// ✅ DOM API 사용 (안전)
function createIcon(name) {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.setAttribute('viewBox', '0 0 24 24');
  // ... 안전하게 요소 추가
  return svg;
}

9. 보안 체크리스트

manifest.json 체크리스트

  • [ ] 불필요한 권한 제거
  • [ ] <all_urls> 대신 구체적인 호스트 패턴
  • [ ] CSP 명시적 설정
  • [ ] web_accessible_resources 최소화 또는 제거
  • [ ] use_dynamic_url: true 사용 (필요시)

코드 체크리스트

  • [ ] 문자열 기반 HTML 삽입 대신 textContent 또는 DOM API
  • [ ] 동적 코드 실행 패턴 사용 금지
  • [ ] 인라인 이벤트 핸들러 제거 (onclick="")
  • [ ] URL 프로토콜 검증
  • [ ] 입력값 길이 제한
  • [ ] 에러 메시지에 민감한 정보 노출 금지

10. 베스트 프랙티스

DO (해야 할 것)

  • [x] CSP를 명시적으로 설정하고 가능한 엄격하게
  • [x] 필요한 권한만 요청
  • [x] Optional Permissions 활용
  • [x] DOM API로 HTML 생성
  • [x] 사용자 입력 검증
  • [x] web_accessible_resources 최소화

DON'T (하지 말아야 할 것)

  • [ ] 'unsafe-eval' 사용
  • [ ] <all_urls> 권한 무분별 사용
  • [ ] 모든 리소스를 web_accessible로 설정
  • [ ] 인라인 스크립트/이벤트 핸들러
  • [ ] 동적 코드 실행 (문자열을 코드로 평가)
  • [ ] 검증 없이 사용자 입력 DOM에 삽입

11. FAQ

Q: 'unsafe-inline'을 style-src에 사용해도 되나요?

A: script-src와 달리 style-src의 'unsafe-inline'상대적으로 덜 위험합니다. CSS로 직접적인 코드 실행은 어렵기 때문입니다. 다만 CSS Injection 공격 가능성은 있으므로 가능하면 피하는 것이 좋습니다.

Q: 외부 CDN 라이브러리를 사용하고 싶어요.

A: Manifest V3에서는 외부 스크립트 로드가 차단됩니다. 라이브러리를 다운로드하여 확장에 포함시키세요. 번들러(webpack, rollup)를 사용하면 더 편리합니다.

Q: CSP 위반이 발생하면 어떻게 확인하나요?

A: 개발자 도구의 Console에서 CSP 위반 메시지를 확인할 수 있습니다:

Refused to execute inline script because it violates the following
Content Security Policy directive: "script-src 'self'"

Q: 다른 확장이나 DevTools에서도 CSP가 적용되나요?

A: 확장 페이지(popup, options 등)에만 적용됩니다. Content Script는 주입된 페이지의 CSP를 따릅니다.

Q: CSP 때문에 기존 코드가 동작하지 않아요.

A: 주로 다음 패턴을 수정해야 합니다:

  1. 인라인 이벤트 핸들러 → addEventListener
  2. 인라인 <script> → 외부 파일
  3. 문자열 기반 타이머 → 함수 기반 타이머
  4. 외부 CDN → 로컬 번들링

12. 참고 자료


시리즈 목차

  1. JavaScript URL 비교와 정규화
  2. Web Crypto API로 안전한 해싱 구현하기
  3. CSS 변수와 다크 모드 구현하기
  4. 크롬 확장 프로젝트 구조 정리하기
  5. 크롬 확장 다국어(i18n) 구현하기
  6. 크롬 확장 공유 모듈 설계
  7. Chrome Storage로 실시간 상태 동기화
  8. Chrome Alarms API로 자동 잠금 타이머
  9. 크롬 확장 보안 강화: CSP와 최소 권한 ← 현재 글