C.W.K.
Stream
Lesson 05 of 06 · published

Iframe 과 CSP — Panel 에서 외부 content 안전하게 load

~13 min · side-panel, csp, iframe, frame-src, web-accessible-resources

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Side panel 은 그냥 extension HTML page, popup 을 다스리는 같은 MV3 content security policy 가 다스려. Lesson 5 는 규칙과 안전한 escape valve — 뭘 직접 렌더 가능, 뭐가 iframe 필요, hole 안 만들고 frame-src 넓히는 법."

Default extension-pages CSP

MV3 가 extension 안 모든 HTML page (popup, side panel, options page, full-tab page) 에 strict default Content Security Policy 와 함께 ship:

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

허용하는 것:

  • Extension 에 bundle 된 JavaScript file (<script src="panel.js">).
  • Extension 에 bundle 된 stylesheet.
  • 이미지 / font / 대부분의 다른 passive resource 는 어디서든.
  • Fetch / XHR 은 어떤 URL 로든.

금지하는 것:

  • Inline script (<script>...</script>) 와 inline event handler (onclick="foo()").
  • eval, new Function, string argument 가진 setTimeout — 다 runtime 에 ban.
  • Extension 바깥 어디서든 <script src="https://cdn.example.com/lib.js"> load.
  • Non-extension URL 가리키는 <iframe> — CSP 안 넓히면 차단.

Remote-script ban 이 MV3 의 flagship 보안 변경. Iframe block 은 side panel 에 third-party content embed 시도할 때 가장 먼저 무는 거.

CSP 넓히기 — frame-src

Panel 에 iframe 허용하려면 manifest 에 content_security_policy.extension_pages entry 추가. 형식은 single CSP string; script-src 와 object-src baseline 다시 진술하고 frame-src 추가:

{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'; frame-src 'self' https://www.youtube.com https://*.figma.com;"
  }
}

이제 <iframe src="https://www.youtube.com/embed/..."> 가 panel.html 안에서 동작. 다른 origin 은 여전히 차단. List 를 가능한 한 tight 하게 유지 — frame-src 의 모든 domain 이 그 site 가 나중에 compromise 되면 잠재 공격 surface.

흔한 use case

  • 문서나 help content. Domain 제어하는 곳에 host 된 자체 docs 렌더; extension 안에 help text bundle 하고 update 하는 것보다 저렴.
  • Third-party widget. Notion page, Figma file, YouTube tutorial embed. 각각 명시적 frame-src entry 필요.
  • Authentication redirect. 일부 auth flow 가 third-party identity provider 통해 redirect; iframe 패턴이 새 tab spawn 없이 panel 안에서 redirect 처리.
  • Sandboxed extension content. 자체 chrome-extension:// page (다른 path 가진) 가리키는 iframe 이 민감 코드 wall off — user-provided HTML/markdown 표시할 때 script-of-self 권한 없이.

제어하는 page sandbox

마지막 케이스 — untrusted user content 렌더링 — 엔 extension iframe 과 sandbox manifest key 결합:

{
  "sandbox": {
    "pages": ["sandbox.html"]
  }
}

sandbox.html 이 chrome.* API access 없이 unique origin 에서 돔. 부모 panel 이 거기 postMessage 가능, 안의 어떤 코드도 user clip storage 닿을 수 없다고 신뢰. Source content 가 본질적으로 임의인 rich markdown 이나 HTML clip 표시 패턴.

Web accessible resources — 역방향

위 iframe 이야기는 panel 이 외부 content load 하는 것. 역방향 — host page 가 extension page embed 하게 허용 — 은 web_accessible_resources 사용:

{
  "web_accessible_resources": [
    {
      "resources": ["injected.js", "badge.html"],
      "matches": ["https://*.github.com/*"]
    }
  ]
}

List 된 origin 이 어떤 extension file load 허용되는지 선언. Extension file 가리키는 <script> 태그 inject 하는 content script (Lesson 3 의 legacy bridge 패턴) 와 page 가 extension iframe embed 하게 허용하는 extension 에서 사용.

Default CSP 가 일부러 tight. 필요한 것만 추가. Inline event handler, eval, 임의 iframe origin 이 design 으로 차단 — underlying 패턴 fix, CSP disable 시도 안 함.
'unsafe-eval' 추가 안 함. Chrome Web Store 가 'unsafe-eval' 이나 'unsafe-inline' 로 CSP 느슨하게 한 extension 을 거부. 드문 정당한 이유 (옛 bundler 의 eval-based loader 통한 WebAssembly) 도 거의 항상 현대 우회로 있음. Eval 필요해 보이면, dependency 가 잘못된 build target 고른 거 — 그걸 fix.

Code

manifest.json — frame-src 가진 extension_pages CSP, plus sandbox page·json
{
  "manifest_version": 3,
  "name": "ClipDeck",
  "version": "0.7.0",
  "action": { "default_popup": "popup.html" },
  "background": { "service_worker": "background.js" },
  "side_panel": { "default_path": "panel.html" },
  "permissions": ["storage", "tabs", "scripting", "activeTab", "sidePanel"],
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'; frame-src 'self' https://docs.example.com;"
  },
  "sandbox": {
    "pages": ["sandbox.html"]
  },
  "content_scripts": [
    { "matches": ["<all_urls>"], "js": ["content.js"], "run_at": "document_idle" }
  ],
  "icons": { "16": "icons/16.png", "48": "icons/48.png", "128": "icons/128.png" }
}
panel.html — third-party iframe + sandboxed extension iframe·html
<!-- panel.html — help doc iframe + sandboxed clip renderer -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="panel.css" />
  </head>
  <body>
    <h1>ClipDeck</h1>
    <details>
      <summary>Help</summary>
      <!-- docs.example.com 이 frame-src 에 있어 허용 -->
      <iframe src="https://docs.example.com/clipdeck-help" width="100%" height="200"></iframe>
    </details>

    <div id="list"></div>

    <!-- untrusted-shape clip (rich HTML/markdown) 용 sandboxed renderer -->
    <iframe id="clipRenderer" src="sandbox.html" style="display:none;width:100%;border:0;"></iframe>

    <script src="panel.js"></script>
  </body>
</html>
panel.js — sandboxed renderer 에 postMessage·javascript
// panel.js — clip 렌더 위해 sandboxed iframe 에 message
function renderClipInSandbox(clip) {
  const iframe = document.getElementById("clipRenderer");
  iframe.style.display = "block";
  iframe.contentWindow.postMessage(
    { source: "clipdeck-panel", type: "renderClip", clip },
    "*"
  );
}

window.addEventListener("message", (event) => {
  // 자체 sandbox iframe 에서 온 메시지만 신뢰
  if (event.source !== document.getElementById("clipRenderer").contentWindow) return;
  if (event.data?.source !== "clipdeck-sandbox") return;
  if (event.data.type === "rendered" && event.data.heightPx) {
    document.getElementById("clipRenderer").style.height = `${event.data.heightPx}px`;
  }
});

// sandbox.html / sandbox.js 의 안쪽이 clip parse, DOM 으로 렌더 (innerHTML 아님 —
// DOMPurify 나 자체 escape 사용), 렌더된 height 측정, postMessage 로 다시 보냄:
//   parent.postMessage({ source: 'clipdeck-sandbox', type: 'rendered', heightPx: ... }, '*');

External links

Exercise

clipdeck/manifest.json 을 첫 번째 code block 으로 update (version 0.7.0, content_security_policy.extension_pages 가 frame-src 에 https://docs.example.com 허용 위해 넓혀짐; sandbox.pages 선언). 두 번째 code block 의 iframe markup 을 clipdeck/panel.html 에 추가. Placeholder sandbox.html 생성: <h1 id="out">sandbox alive</h1><script>parent.postMessage({source:'clipdeck-sandbox', type:'rendered', heightPx:40}, '*');</script>. Extension reload 하고 side panel 열기 — docs iframe 은 placeholder error page 표시 (docs.example.com 은 실제 host 아님), sandbox iframe 이 load 되고 postMessage 보냄. Panel DevTools 의 network tab 열어 docs.example.com 요청 허용된 거 확인; 아니면 CSP error 로깅됐을 거. CSP 가 동작 증명하려면 iframe src 를 https://example.com/anything (frame-src 에 없음) 으로 바꾸고 reload — iframe 이 console 의 명확한 CSP violation 과 함께 차단됨.
Hint
Panel DevTools console 이 manifest update 후에도 Refused to frame ... because it violates the following Content Security Policy directive: frame-src 'self' 보고하면, 두 가지 흔한 원인: (1) manifest 편집 후 extension reload 안 함, 또는 (2) manifest JSON 이 문법적으로 깨져 Chrome 이 silently default 로 fallback (chrome://extensions → Errors 확인). Sandbox.html 이 아예 load 안 되면 sandbox.pages 아래 list 됐고 extension 디렉토리의 실제 파일인지 확인. Sandbox postMessage 가 sandbox.js 가 chrome.* access 시도하면 silently fail — sandbox page 는 chrome.* API access 없음, 그게 point.

Progress

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

댓글 0

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

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