본문 바로가기
C.W.K.
Stream
Lesson 05 of 06 · published

MV3 보안 모델 — Sandbox, CSP, Permissions

~12 min · mv3, security, csp, permissions, host_permissions, sandbox

Level 0Extension 입덕
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"MV3의 보안 모델은 까칠할 정도로 의견이 강해 — 그게 버그가 아니라 feature."

Extension Sandbox

모든 Chrome extension은 자기 renderer process에서 돌아, 상호작용하는 host page와 격리. Extension은 chrome.* API와 사적 origin (chrome-extension://<id>/)에 접근하지만, host page의 JavaScript context에 직접 손 못 대. 통신은 의도된 채널로만 — message / isolated world에 injected 된 content script / Chrome의 tab API.

이 격리가 extension이 user-script injector 보다 안전한 전체 이유. Host page가 extension storage를 손쉽게 못 읽고, extension이 host의 global scope를 실수로 오염시킬 수도 없어.

MV3의 Content Security Policy

MV3는 extension page (popup / options / side panel)에 빡센 default CSP를 박아:

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

풀어 쓰면 이래. script는 extension package 안에 있는 것만 돌아. inline <script> 태그 안 되고, eval() 안 되고, new Function() 안 되고, handler에 JavaScript URI도 안 돼. Lesson 4에서 만난 CSP gotcha가 바로 이 default에서 나온 거야.

CSP를 더 조일 수는 있어. 드물지만 가능해. 반대로 푸는 건 아주 빡빡하게 막혀 있고. 'unsafe-eval'은 완전 금지. 'unsafe-inline'은 script에 대해 금지. WebAssembly compilation 진짜 필요하면 'wasm-unsafe-eval' 허용.

host_permissions — 어떤 page를 만질 수 있어

API permissions와 별개로 host_permissions는 민감한 tab field 읽기, cross-origin fetch, scripting을 통한 programmatic injection 같은 URL 범위 능력을 열어. 이것만 쓴다고 script가 자동 injection되지는 않아. 정적 injection은 content_scripts.matches, 동적 injection은 scripting 같은 API가 따로 맡아.

  • 좁게 — 특정 origin list: ["https://news.ycombinator.com/*", "https://*.github.com/*"]. Chrome이 install 시 user 한테 정확히 어떤 site가 영향받는지 보여줌.
  • 광범위["<all_urls>"]. 살벌한 install 경고 발동: "모든 website의 모든 data를 읽고 변경." Extension이 진짜로 어디든 page access 필요할 때만 (ClipDeck은 결국 필요 — selection capture가 어느 page 에서든 동작해야).

중간 길이 하나 있어. API permissionsactiveTab을 넣는 거야. 필요한 순간에만 주는 방식이지 — user가 toolbar icon을 누르면 Chrome이 그 tab의 민감한 metadata와 origin 접근을 잠깐 열어 주고, DOM injection 자체는 chrome.scripting이나 이미 선언한 content script가 맡고, 그 tab이 다른 데로 넘어가면 도로 닫혀. Install 할 때 경고도 안 뜨고, 넓은 권한을 계속 들고 있지도 않아.

Remote code 금지

MV3는 network로 JavaScript fetch 후 실행 금지. 구체적으로:

  • fetch("https://...") 후 response에 eval() 안 됨.
  • Extension HTML page에 <script src="https://cdn.example.com/foo.js"> 안 됨.
  • new Function(remoteString) 안 됨.

허용되는 거: network로 fetch 한 data (JSON / text / image)를 data로 다루기. 경계선은 실행. Server가 extension 한테 뭘 렌더하라고 알려 — OK. Server가 extension 한테 뭘 실행하라고 알려 — 금지.

chrome.permissions API — 필요할 때 요청

optional_permissions 또는 optional_host_permissions에 선언된 permission은 extension이 runtime에 chrome.permissions.request로 요청 가능. 그러면 Chrome이 확인 창을 띄우고 user가 허락하거나 거절해. 다 미리 선언해 놓고 install 할 때 사람 놀래키는 것보다 훨씬 예의 있는 방식이지.

ClipDeck은 필요한 permission을 전부 미리 선언한 채로 갈 거야. 사적인 tailnet 안에서 혼자 쓰는 도구한테는 optional-permission flow가 딱히 쓸 데가 없거든. 그래도 이런 API가 있다는 건 알아 둬. 여러 Mac에 뿌릴 extension을 설계할 때, install 화면부터 사람 겁주고 싶지 않다면 그때 이게 필요해져.

MV3 보안은 default가 이미 빡세. 설정으로 더 조일 수는 있어도, 푸는 쪽은 겹겹이 막혀 있고 감사도 따라붙어. 웬만하면 default 안에서 만들고, 어쩔 수 없이 밖으로 나가야 할 땐 언제 다시 걷어낼 건지를 같이 적어 둬.
CSP 에러 메시지는 유난히 cryptic 해. Popup 이나 side panel이 silent 하게 JS 렌더 실패할 때, Errors panel이 "Refused to execute inline script" 랑 줄 번호만 딱 던져 주고 어느 파일 인지는 안 알려주는 경우가 많아. 이럴 땐 코드 전체에서 inline <script> 태그랑 onclick= handler를 훑는 게 제일 빠른 복구 경로야.

Code

MV3 CSP — default vs Pippa Chrome Embed의 iframe-허용 override·json
// Default MV3 CSP for extension_pages (you don't have to specify this):
{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self';"
  }
}

// current Pippa ChromeEmbed loosens it slightly to allow iframe:
{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'; connect-src http://localhost:5173 http://127.0.0.1:5173 http://100.x.x.x:5173; frame-src http://localhost:5173 http://127.0.0.1:5173 http://100.x.x.x:5173;"
  }
}
// Public quest excerpt: 100.x.x.x redacts the private Tailnet origin. Substitute the actual local origin before loading the extension.
Permission scope 세 가지 — narrow / broad / activeTab·json
// Narrow — Chrome shows specific sites at install:
{
  "host_permissions": [
    "https://news.ycombinator.com/*",
    "https://*.github.com/*"
  ]
}

// Broad — Chrome shows "all websites" warning:
{
  "host_permissions": ["<all_urls>"]
}

// activeTab — no install warning, just-in-time grant on toolbar click:
{
  "permissions": ["activeTab"]
}

External links

Exercise

clipdeck/manifest.json 열어. 지금은 permissions field 자체가 없어 — Track 1의 hello-world ClipDeck은 chrome.tabs.query로 active tab title만 읽고, 그건 명시적 permission 없이 됨. manifest에 "permissions": ["activeTab"] 추가하고 reload. Toolbar 아이콘 클릭 — 동작 같아? 이제 activeTab 빼고 대신 "host_permissions": ["<all_urls>"] 추가. Reload, chrome://extensions에서 ClipDeck의 "Details" 클릭. "Site access" 까지 스크롤 — 이제 "On all sites" 라고 떠 (다른 사람한테 ship 했으면 install 시 경고 보였을 거). Popup 전용 extension 한테 왜 activeTab이 맞는 선택이고, ClipDeck의 이 단계에서 host_permissions가 왜 과한지 한 문장으로 써.
Hint
activeTab은 user 클릭 시에만 임시 access 부여 — install 경고 없음. host_permissions는 install부터 영구 access — 경고 발동. ClipDeck도 뒤쪽 track (content script, 페이지 넘나드는 selection capture)에 가면 host_permissions가 필요해져. Track 1 에선 아직 아니고. 권한은 실제로 쓰는 기능만큼만 넓히는 게 맞아.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파serious

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.