콘텐츠 스크립트

콘텐츠 스크립트는 웹페이지의 컨텍스트에서 실행되는 파일입니다. 표준 문서 객체 모델 (DOM)을 사용하여 브라우저가 방문하는 웹페이지의 세부정보를 읽고, 이를 변경하고, 상위 확장 프로그램에 정보를 전달할 수 있습니다.

콘텐츠 스크립트 기능 이해하기

콘텐츠 스크립트는 다음 확장 프로그램 API에 직접 액세스할 수 있습니다.

콘텐츠 스크립트는 다른 API에 직접 액세스할 수 없습니다. 하지만 확장 프로그램의 다른 부분과 메시지를 교환하여 간접적으로 액세스할 수 있습니다.

fetch()와 같은 API를 사용하여 콘텐츠 스크립트에서 확장 프로그램의 다른 파일에 액세스할 수도 있습니다. 이렇게 하려면 웹 액세스 가능 리소스로 선언해야 합니다. 이렇게 하면 동일한 사이트에서 실행되는 모든 서드 파티 또는 퍼스트 파티 스크립트에 리소스가 노출됩니다.

격리된 세계에서 작업

콘텐츠 스크립트는 격리된 환경에 있으므로 콘텐츠 스크립트가 페이지 또는 다른 확장 프로그램의 콘텐츠 스크립트와 충돌하지 않고 JavaScript 환경을 변경할 수 있습니다.

확장 프로그램은 다음 예와 유사한 코드로 웹페이지에서 실행될 수 있습니다.

webPage.html

<html>
  <button id="mybutton">click me</button>
  <script>
    var greeting = "hello, ";
    var button = document.getElementById("mybutton");
    button.person_name = "Bob";
    button.addEventListener(
        "click", () => alert(greeting + button.person_name + "."), false);
  </script>
</html>

이 확장 프로그램은 스크립트 삽입 섹션에 설명된 기법 중 하나를 사용하여 다음 콘텐츠 스크립트를 삽입할 수 있습니다.

content-script.js

var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener(
    "click", () => alert(greeting + button.person_name + "."), false);

이 변경으로 버튼을 클릭하면 두 알림이 순서대로 표시됩니다.

스크립트 삽입

콘텐츠 스크립트는 정적으로 선언하거나 동적으로 선언하거나 프로그래매틱 방식으로 삽입할 수 있습니다.

정적 선언으로 삽입

잘 알려진 페이지 집합에서 자동으로 실행되어야 하는 스크립트의 경우 manifest.json에서 정적 콘텐츠 스크립트 선언을 사용합니다.

정적으로 선언된 스크립트는 매니페스트에서 "content_scripts" 키 아래에 등록됩니다. JavaScript 파일, CSS 파일 또는 둘 다 포함할 수 있습니다. 모든 자동 실행 콘텐츠 스크립트는 일치 패턴을 지정해야 합니다.

manifest.json

{
 "name": "My extension",
 ...
 "content_scripts": [
   {
     "matches": ["https://*.nytimes.com/*"],
     "css": ["my-styles.css"],
     "js": ["content-script.js"]
   }
 ],
 ...
}

이름 유형 설명
matches 문자열 배열 필수사항. 이 콘텐츠 스크립트가 삽입될 페이지를 지정합니다. 이러한 문자열의 문법에 대한 자세한 내용은 일치 패턴을 참고하고 URL을 제외하는 방법에 대한 자세한 내용은 일치 패턴 및 glob을 참고하세요.
css 문자열 배열 선택사항. 일치하는 페이지에 삽입할 CSS 파일 목록입니다. 이러한 스타일은 페이지의 DOM이 구성되거나 표시되기 전에 이 배열에 표시되는 순서대로 삽입됩니다.
js 문자열 배열 선택사항. 일치하는 페이지에 삽입할 JavaScript 파일 목록입니다. 파일은 이 배열에 표시된 순서대로 삽입됩니다. 이 목록의 각 문자열에는 확장 프로그램의 루트 디렉터리에 있는 리소스의 상대 경로가 포함되어야 합니다. 앞쪽 슬래시 (`/`)는 자동으로 잘립니다.
run_at RunAt 선택사항. 스크립트를 페이지에 삽입해야 하는 시점을 지정합니다. 기본값은 document_idle입니다.
match_about_blank 부울 선택사항. 스크립트가 상위 또는 오프너 프레임이 matches에 선언된 패턴 중 하나와 일치하는 about:blank 프레임에 삽입되어야 하는지 여부입니다. 기본값은 false입니다.
match_origin_as_fallback 부울 선택사항. 스크립트가 일치하는 출처로 생성되었지만 URL 또는 출처가 패턴과 직접 일치하지 않을 수 있는 프레임에 삽입되어야 하는지 여부입니다. 여기에는 about:, data:, blob:, filesystem:와 같은 다양한 스키마가 있는 프레임이 포함됩니다. 관련 프레임에 삽입도 참고하세요.
world ExecutionWorld 선택사항. 스크립트가 실행될 JavaScript 세계입니다. 기본값은 ISOLATED입니다. 격리된 세계에서 작업하기도 참고하세요.

동적 선언으로 삽입

동적 콘텐츠 스크립트는 콘텐츠 스크립트의 일치 패턴을 잘 모르는 경우나 콘텐츠 스크립트를 알려진 호스트에 항상 삽입해서는 안 되는 경우에 유용합니다.

Chrome 96에서 도입된 동적 선언은 정적 선언과 유사하지만 콘텐츠 스크립트 객체는 manifest.json이 아닌 chrome.scripting 네임스페이스의 메서드를 사용하여 Chrome에 등록됩니다. 스크립팅 API를 사용하면 확장 프로그램 개발자가 다음 작업을 할 수 있습니다.

정적 선언과 마찬가지로 동적 선언에는 JavaScript 파일, CSS 파일 또는 둘 다 포함될 수 있습니다.

service-worker.js

chrome.scripting
  .registerContentScripts([{
    id: "session-script",
    js: ["content.js"],
    persistAcrossSessions: false,
    matches: ["*://example.com/*"],
    runAt: "document_start",
  }])
  .then(() => console.log("registration complete"))
  .catch((err) => console.warn("unexpected error", err))

service-worker.js

chrome.scripting
  .updateContentScripts([{
    id: "session-script",
    excludeMatches: ["*://admin.example.com/*"],
  }])
  .then(() => console.log("registration updated"));

service-worker.js

chrome.scripting
  .getRegisteredContentScripts()
  .then(scripts => console.log("registered content scripts", scripts));

service-worker.js

chrome.scripting
  .unregisterContentScripts({ ids: ["session-script"] })
  .then(() => console.log("un-registration complete"));

프로그래매틱 방식으로 삽입

이벤트에 응답하거나 특정 상황에서 실행해야 하는 콘텐츠 스크립트에는 프로그래매틱 삽입을 사용하세요.

콘텐츠 스크립트를 프로그래매틱 방식으로 삽입하려면 스크립트를 삽입하려는 페이지에 대한 호스트 권한이 확장 프로그램에 있어야 합니다. 호스트 권한은 확장 프로그램의 매니페스트의 일부로 요청하여 부여하거나 "activeTab"을 일시적으로 사용하여 부여할 수 있습니다.

다음은 activeTab 기반 확장 프로그램의 다른 버전입니다.

manifest.json:

{
  "name": "My extension",
  ...
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_title": "Action Button"
  }
}

콘텐츠 스크립트는 파일로 삽입할 수 있습니다.

content-script.js


document.body.style.backgroundColor = "orange";

service-worker.js:

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ["content-script.js"]
  });
});

또는 함수 본문을 콘텐츠 스크립트로 삽입하고 실행할 수 있습니다.

service-worker.js:

function injectedFunction() {
  document.body.style.backgroundColor = "orange";
}

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
  });
});

삽입된 함수는 chrome.scripting.executeScript() 호출에서 참조된 함수의 사본이며 원래 함수 자체가 아닙니다. 따라서 함수의 본문은 자체적으로 포함되어야 합니다. 함수 외부의 변수를 참조하면 콘텐츠 스크립트에서 ReferenceError이 발생합니다.

함수로 삽입할 때는 함수에 인수를 전달할 수도 있습니다.

service-worker.js

function injectedFunction(color) {
  document.body.style.backgroundColor = color;
}

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
    args : [ "orange" ],
  });
});

일치 및 glob 제외

지정된 페이지 일치를 맞춤설정하려면 선언적 등록에 다음 필드를 포함하세요.

이름 유형 설명
exclude_matches 문자열 배열 선택사항. 이 콘텐츠 스크립트가 삽입될 수 있는 페이지를 제외합니다. 이러한 문자열의 문법에 대한 자세한 내용은 일치 패턴을 참고하세요.
include_globs 문자열 배열 선택사항. 이 glob과도 일치하는 URL만 포함하도록 matches 후에 적용됩니다. 이는 Greasemonkey 키워드인 @include를 에뮬레이트하기 위한 것입니다.
exclude_globs 문자열 배열 선택사항. 이 glob과 일치하는 URL을 제외하기 위해 matches 뒤에 적용됩니다. @exclude Greasemonkey 키워드를 에뮬레이션하기 위한 것입니다.

다음 두 가지가 모두 참인 경우 콘텐츠 스크립트가 페이지에 삽입됩니다.

  • URL이 matches 패턴 및 include_globs 패턴과 일치합니다.
  • URL이 exclude_matches 또는 exclude_globs 패턴과도 일치하지 않습니다. matches 속성은 필수이므로 exclude_matches, include_globs, exclude_globs는 영향을 받는 페이지를 제한하는 데만 사용할 수 있습니다.

다음 확장 프로그램은 콘텐츠 스크립트를 https://www.nytimes.com/health에 삽입하지만 https://www.nytimes.com/business에는 삽입하지 않습니다 .

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  excludeMatches : [ "*://*/*business*" ],
  js : [ "contentScript.js" ],
}]);

Glob 속성은 일치 패턴과 다른 더 유연한 문법을 따릅니다. 허용되는 glob 문자열은 '와일드 카드' 별표와 물음표를 포함할 수 있는 URL입니다. 별표 (*)는 빈 문자열을 포함하여 모든 길이의 문자열과 일치하며 물음표 (?)는 모든 단일 문자와 일치합니다.

예를 들어 glob https://???.example.com/foo/\*는 다음 중 하나와 일치합니다.

  • https://www.example.com/foo/bar
  • https://the.example.com/foo/

하지만 다음과는 일치하지 않습니다.

  • https://my.example.com/foo/bar
  • https://example.com/foo/
  • https://www.example.com/foo

이 확장 프로그램은 콘텐츠 스크립트를 https://www.nytimes.com/arts/index.htmlhttps://www.nytimes.com/jobs/index.htm*에 삽입하지만 https://www.nytimes.com/sports/index.html에는 삽입하지 않습니다.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "include_globs": ["*nytimes.com/???s/*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

이 확장 프로그램은 콘텐츠 스크립트를 https://history.nytimes.comhttps://.nytimes.com/history에 삽입하지만 https://science.nytimes.com 또는 https://www.nytimes.com/science에는 삽입하지 않습니다.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

올바른 범위를 달성하기 위해 이러한 범위 중 하나, 전부 또는 일부를 포함할 수 있습니다.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "include_globs": ["*nytimes.com/???s/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

실행 시간

run_at 필드는 JavaScript 파일이 웹페이지에 삽입되는 시점을 제어합니다. 기본값은 "document_idle"입니다. 가능한 다른 값은 RunAt 유형을 참고하세요.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "run_at": "document_idle",
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  runAt : "document_idle",
  js : [ "contentScript.js" ],
}]);
이름 유형 설명
document_idle 문자열 선호. 가능한 경우 "document_idle"를 사용하세요.

브라우저가 "document_end"window.onload 이벤트가 발생한 직후 사이에 스크립트를 삽입할 시간을 선택합니다. 삽입되는 정확한 시점은 문서의 복잡성과 로드되는 데 걸리는 시간에 따라 다르며 페이지 로드 속도에 맞게 최적화됩니다.

"document_idle"에서 실행되는 콘텐츠 스크립트는 window.onload 이벤트를 수신 대기할 필요가 없습니다. DOM이 완료된 후에 실행되도록 보장되기 때문입니다. 스크립트가 window.onload 후에 실행되어야 하는 경우 확장 프로그램은 document.readyState 속성을 사용하여 onload이 이미 실행되었는지 확인할 수 있습니다.
document_start 문자열 스크립트는 css의 파일 뒤에 삽입되지만 다른 DOM이 생성되거나 다른 스크립트가 실행되기 전에 삽입됩니다.
document_end 문자열 스크립트는 DOM이 완료된 직후에 삽입되지만 이미지 및 프레임과 같은 하위 리소스가 로드되기 전입니다.

프레임 지정

매니페스트에 지정된 선언적 콘텐츠 스크립트의 경우 "all_frames" 필드를 사용하면 확장 프로그램이 지정된 URL 요구사항과 일치하는 모든 프레임에 JavaScript 및 CSS 파일을 삽입할지 아니면 탭의 최상위 프레임에만 삽입할지 지정할 수 있습니다.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "all_frames": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}

chrome.scripting.registerContentScripts(...)을 사용하여 콘텐츠 스크립트를 프로그래매틱 방식으로 등록할 때 allFrames 매개변수를 사용하여 콘텐츠 스크립트를 지정된 URL 요구사항과 일치하는 모든 프레임에 삽입할지 아니면 탭의 최상위 프레임에만 삽입할지 지정할 수 있습니다. 이는 tabId와 함께만 사용할 수 있으며 frameIds 또는 documentIds가 지정된 경우에는 사용할 수 없습니다.

service-worker.js

chrome.scripting.registerContentScripts([{
  id: "test",
  matches : [ "https://*.nytimes.com/*" ],
  allFrames : true,
  js : [ "contentScript.js" ],
}]);

확장 프로그램은 일치하는 프레임과 관련이 있지만 자체적으로는 일치하지 않는 프레임에서 스크립트를 실행할 수 있습니다. 이러한 경우가 발생하는 일반적인 시나리오는 일치하는 프레임에 의해 생성되었지만 URL 자체가 스크립트의 지정된 패턴과 일치하지 않는 URL이 있는 프레임입니다.

이는 확장 프로그램이 about:, data:, blob:, filesystem: 스키마가 있는 URL이 포함된 프레임에 삽입하려는 경우에 해당합니다. 이러한 경우 URL이 콘텐츠 스크립트의 패턴과 일치하지 않으며 (about:data:의 경우 URL에 상위 URL이나 출처가 전혀 포함되지 않음, 예: about:blank 또는 data:text/html,<html>Hello, World!</html>), 이러한 프레임은 생성 프레임과 계속 연결될 수 있습니다.

이러한 프레임에 삽입하기 위해 확장 프로그램은 매니페스트의 콘텐츠 스크립트 사양에 "match_origin_as_fallback" 속성을 지정할 수 있습니다.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.google.com/*"],
      "match_origin_as_fallback": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}

지정되고 true로 설정되면 Chrome은 프레임 자체의 URL이 아닌 프레임 이니시에이터의 출처를 확인하여 프레임이 일치하는지 확인합니다. 이는 타겟 프레임의 출처 (예: data: URL의 출처가 null입니다.

프레임의 이니시에이터는 타겟 프레임을 만들거나 탐색한 프레임입니다. 일반적으로 직접 상위 요소 또는 오프너이지만 iframe 내에서 iframe을 탐색하는 프레임의 경우와 같이 그렇지 않을 수도 있습니다.

이는 이니시에이터 프레임의 출처를 비교하므로 이니시에이터 프레임은 해당 출처의 모든 경로에 있을 수 있습니다. 이 의미를 명확하게 하기 위해 Chrome에서는 "match_origin_as_fallback"true으로 설정된 콘텐츠 스크립트가 * 경로도 지정해야 합니다.

"match_origin_as_fallback""match_about_blank"이 모두 지정된 경우 "match_origin_as_fallback"이 우선 적용됩니다.

삽입 페이지와의 통신

콘텐츠 스크립트의 실행 환경과 콘텐츠 스크립트를 호스팅하는 페이지는 서로 격리되어 있지만 페이지의 DOM에 대한 액세스 권한은 공유합니다. 페이지가 콘텐츠 스크립트 또는 콘텐츠 스크립트를 통해 확장 프로그램과 통신하려면 공유 DOM을 통해 통신해야 합니다.

window.postMessage()을 사용하여 예를 들 수 있습니다.

content-script.js

var port = chrome.runtime.connect();

window.addEventListener("message", (event) => {
  // We only accept messages from ourselves
  if (event.source !== window) {
    return;
  }

  if (event.data.type && (event.data.type === "FROM_PAGE")) {
    console.log("Content script received: " + event.data.text);
    port.postMessage(event.data.text);
  }
}, false);

example.js

document.getElementById("theButton").addEventListener("click", () => {
  window.postMessage(
      {type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);

확장 프로그램이 아닌 페이지(example.html)는 자체적으로 메시지를 게시합니다. 이 메시지는 콘텐츠 스크립트에 의해 가로채기되고 검사된 후 확장 프로그램 프로세스에 게시됩니다. 이렇게 하면 페이지에서 확장 프로그램 프로세스와의 통신 라인이 설정됩니다. 유사한 수단을 통해 반대도 가능합니다.

확장 프로그램 파일 액세스

콘텐츠 스크립트에서 확장 프로그램 파일에 액세스하려면 다음 예시 (content.js)와 같이 chrome.runtime.getURL()를 호출하여 확장 프로그램 애셋의 절대 URL을 가져오면 됩니다.

content-script.js

let image = chrome.runtime.getURL("images/my_image.png")

CSS 파일에서 글꼴이나 이미지를 사용하려면 다음 예 (content.css)와 같이 @@extension_id를 사용하여 URL을 구성하면 됩니다.

content.css

body {
 background-image:url('chrome-extension://__MSG_@@extension_id__/background.png');
}

@font-face {
 font-family: 'Stint Ultra Expanded';
 font-style: normal;
 font-weight: 400;
 src: url('chrome-extension://__MSG_@@extension_id__/fonts/Stint Ultra Expanded.woff') format('woff');
}

모든 애셋은 manifest.json 파일에서 웹 액세스 가능 리소스로 선언해야 합니다.

manifest.json

{
 ...
 "web_accessible_resources": [
   {
     "resources": [ "images/*.png" ],
     "matches": [ "https://example.com/*" ]
   },
   {
     "resources": [ "fonts/*.woff" ],
     "matches": [ "https://example.com/*" ]
   }
 ],
 ...
}

콘텐츠 보안 정책

격리된 세계에서 실행되는 콘텐츠 스크립트에는 다음 콘텐츠 보안 정책 (CSP)이 있습니다.

script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';

다른 확장 프로그램 컨텍스트에 적용되는 제한사항과 마찬가지로 이렇게 하면 eval()를 사용할 수 없으며 외부 스크립트를 로드할 수 없습니다.

압축 해제된 확장 프로그램의 경우 CSP에는 localhost도 포함됩니다.

script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:* chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';

콘텐츠 스크립트가 기본 세계에 삽입되면 페이지의 CSP가 적용됩니다.

보안 유지

격리된 세계는 보호 레이어를 제공하지만 콘텐츠 스크립트를 사용하면 확장 프로그램과 웹페이지에 취약점이 발생할 수 있습니다. 콘텐츠 스크립트가 fetch()를 호출하는 등 별도의 웹사이트에서 콘텐츠를 수신하는 경우 콘텐츠를 삽입하기 전에 교차 사이트 스크립팅 공격에 대해 콘텐츠를 필터링해야 합니다. "man-in-the-middle" 공격을 방지하기 위해 HTTPS를 통해서만 통신합니다.

악성 웹페이지를 필터링해야 합니다. 예를 들어 다음 패턴은 위험하며 매니페스트 V3에서 허용되지 않습니다.

금지사항

content-script.js

const data = document.getElementById("json-data");
// WARNING! Might be evaluating an evil script!
const parsed = eval("(" + data + ")");
금지사항

content-script.js

const elmt_id = ...
// WARNING! elmt_id might be '); ... evil script ... //'!
window.setTimeout("animate(" + elmt_id + ")", 200);

대신 스크립트를 실행하지 않는 더 안전한 API를 사용하는 것이 좋습니다.

권장사항

content-script.js

const data = document.getElementById("json-data")
// JSON.parse does not evaluate the attacker's scripts.
const parsed = JSON.parse(data);
권장사항

content-script.js

const elmt_id = ...
// The closure form of setTimeout does not evaluate scripts.
window.setTimeout(() => animate(elmt_id), 200);