Home SDK 스크립트 연동 원리와 브라우저 보안 정책
Post
Cancel

SDK 스크립트 연동 원리와 브라우저 보안 정책


최근 AI에 대한 공부를 하다가, SDK를 연동해 광고 데이터를 수집하는 것을 알게 됐습니다. 이 과정에서 SDK 연동 원리가 궁금해졌고, 이를 정리하기 위해 글을 작성하게 되었습니다.




1. SDK 스크립트 연동 원리


고객사 사이트에 채팅 위젯을 심는 구조는 외부 자바스크립트 파일 하나를 로드해서, 그 스크립트가 고객사 DOM에 위젯을 그리고, 우리 서버와 통신하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
고객사 HTML (example.com)
  │
  │  <script src="https://api.supple-talk.com/sdk/widget.js">                                                                                                                                  
  │                                                                                                                        
  │
  ├─ 1. widget.js 다운로드 (Cross-Origin Embedding)                                                                                                                                              
  │     example.com ──GET──▶ api.supple-talk.com
  │
  ├─ 2. 고객사 DOM에 위젯 생성
  │     <div id="supple-talk-root">
  │       └─ React ChatWidget 마운트
  │
  └─ 3. 우리 서버와 통신 (Cross-Origin Read → CORS)
        브라우저 ──fetch()──▶ api.supple-talk.com




1-1. 고객사가 스크립트 태그를 삽입

고객사 개발자가 자기 HTML의 <body> 하단에 아래 코드를 넣습니다. 이 코드는 우리 서버에서 widget.js 파일을 비동기로 다운받아서 페이지에 삽입 합니다. 고객사별 API 키를 넣어서 어떤 고객사인지 식별하고요.

1
2
3
4
5
6
7
8
9
<script>
  (function() {
    var s = document.createElement('script');
    s.src = 'https://api.supple-talk.com/sdk/widget.js';
    s.async = true;
    s.dataset.key = 'CUSTOMER_API_KEY_HERE';
    document.body.appendChild(s);
  })();
</script>




1-2. 위젯 마운트

widget.js는 우리가 빌드해서 서버에 올려두는 파일로, React로 만든 ChatWidget을 번들링한 파일입니다. 이 파일이 로드되면 자동으로 실행되며, 고객사 페이지의 기존 코드에 영향을 주지 않으면서, <div id="supple-talk-root">를 하나 만들고 그 안에 우리 위젯을 렌더링 합니다. React, CSS 등 필요한 모든 것이 widget.js 한 파일에 다 들어있어서 고객사가 별도로 설치할 것은 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function() {
  // 1. API 키 추출
  var script = document.currentScript || document.querySelector('script[data-key]');
  var apiKey = script.dataset.key;

  // 2. 위젯을 담을 div를 고객사 페이지에 생성
  var container = document.createElement('div');
  container.id = 'supple-talk-root';
  document.body.appendChild(container);

    // 3. React 앱(ChatWidget)을 그 div에 마운트. 빌드 시 React, ReactDOM, ChatWidget이 이 파일 안에 전부 번들링되어 있음
  ReactDOM.createRoot(container).render(
    React.createElement(ChatWidget, { apiKey: apiKey })
  );
})();




1-3. 위젯과 서버 간 통신

위젯이 마운트되면 우리 API 서버에 요청을 보냅니다. 차이점은 API_BASE가 우리 실제 도메인이 되고, 요청마다 API 키가 포함된다는 것뿐입니다. 고객사 서버를 경유하지 않고, 고객사 유저의 브라우저에서 우리 서버로 직접 HTTP 요청을 보내는 겁니다. 도메인이 다르기 때문에 우리 서버에 CORS 설정이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
고객사 브라우저 (example.com 페이지)
│
│  widget.js가 로드되어 실행 중
│
├─ POST https://api.supple-talk.com/v1/chat/rooms
│  Headers: { X-API-Key: 'CUSTOMER_API_KEY' }
│
├─ POST https://api.supple-talk.com/v1/chat/rooms/{id}/messages
│  Headers: { X-API-Key: 'CUSTOMER_API_KEY' }
│  Body: { message: '환불하고 싶어요' }
│  Response: SSE 스트리밍
│
└─ GET https://api.supple-talk.com/v1/chat/rooms/{id}/messages
   Headers: { X-API-Key: 'CUSTOMER_API_KEY' }




1-4. 고객사 식별

우리 서버는 요청에 포함된 API 키로 어떤 고객사인지 판별합니다. 고객사마다 별도 테넌트로 관리되니까 데이터가 섞이지 않습니다. 이후는 외부 통신과 같습니다.

1
2
3
# 어떤 고객사인지 조회
api_key = request.headers.get("X-API-Key")
tenant = await find_tenant_by_key(api_key)





2. 브라우저 보안 정책: 페이지 리소스 로딩 vs 데이터 접근


브라우저는 리소스 종류에 따라 보안 정책을 다르게 적용합니다.



2-1. 제한 없는 것들

페이지를 구성하는 리소스는 제한이 없습니다. 브라우저는 태초부터 여러 서버의 리소스를 조합해서 페이지를 만들도록 설계됐습니다. <script src>, <img src>, <link href>, <video src> 같은 태그로 외부 리소스를 가져오는 건 웹의 기본 동작이라 제한이 없습니다.

  • JavaScript with <script src="…"></script>. Error details for syntax errors are only available for same-origin scripts.
  • CSS applied with <link rel="stylesheet" href="…">. Due to the relaxed syntax rules of CSS, cross-origin CSS requires a correct Content-Type header. Browsers block stylesheet loads if it is a cross-origin load where the MIME type is incorrect and the resource does not start with a valid CSS construct.
  • Images displayed by <img>.
  • Media played by <video> and <audio>.
  • External resources embedded with <object> and <embed>.
  • Fonts applied with @font-face. Some browsers allow cross-origin fonts, others require same-origin.
  • Anything embedded by <iframe>. Sites can use the X-Frame-Options header to prevent cross-origin framing.




1990년대부터 이렇게 동작했고, 이걸 막으면 웹 자체가 작동을 안 합니다. 아래와 같은 코드를 만나면 브라우저는 단순히 그 파일을 다운받아서 현재 페이지에서 실행합니다. 브라우저 입장에서는 “페이지 만드는 데 필요한 재료를 가져온 것”일 뿐입니다.

1
<script src="https://다른서버/파일.js">




2-2. 제한 있는 것들

코드가 데이터를 읽으려는 행위는 제한이 있습니다. fetch( )나 XMLHttpRequest는 성격이 다릅니다. 이건 자바스크립트 코드가 다른 서버에 요청을 보내고 응답 데이터를 읽는 행위입니다. 만약 이게 자유로우면 악성 스크립트가 유저의 쿠키/세션을 가지고 다른 사이트의 API를 몰래 호출해서 개인정보를 빼갈 수 있습니다.

For security reasons, browsers restrict cross-origin HTTP requests initiated from scripts. For example, fetch() and XMLHttpRequest follow the same-origin policy. This means that a web application using those APIs can only request resources from the same origin the application was loaded from unless the response from other origins includes the right CORS headers.




그래서 브라우저는 fetch() 응답을 받으면 “이 서버가 너한테 데이터를 줘도 된다고 했어?”를 확인합니다. 그게 CORS입니다. 서버가 Access-Control-Allow-Origin 헤더로 “이 도메인에서 오는 요청은 허용한다”고 명시해야 브라우저가 응답을 자바스크립트에 넘겨줍니다. 결국 브라우저의 기준은 “자바스크립트 코드가 다른 도메인의 데이터를 읽을 수 있느냐”입니다. 읽을 수 있으면 제한하고, 실행만 하는 거면 허용합니다.

  • <script src>: 코드를 실행만 함. 응답 내용을 자바스크립트 변수로 읽을 수 없음
  • fetch( ): 응답 데이터를 자바스크립트가 읽을 수 있음. 그래서 위험하고, 그래서 CORS로 통제함





3. script가 허용되는 이유


3-1. 실행은 허용, 읽기는 제한

브라우저의 보안 모델은 실행읽기 를 구분합니다.

1
<script src="https://evil.com/script.js"></script>




이 스크립트는 실행되지만, 아래 행위는 Same-Origin Policy 때문에 불가능합니다. 즉, 브라우저의 보안 모델은 외부 코드 실행은 허용하되, 데이터 접근은 Origin 단위로 제한하는 것입니다.

1
2
3
4
5
6
evil.com에서 로드된 스크립트가 할 수 없는 것
  │
  ├─ example.com의 쿠키 읽기              → Origin이 다름
  ├─ example.com의 localStorage 읽기      → Origin이 다름
  ├─ example.com의 DOM 접근               → Origin이 다름
  └─ fetch()로 다른 서버 데이터 읽기        → CORS 필요




3-2. JSONP

CORS가 표준화되기 전, 개발자들은

1
<script src="https://api.server.com/data?callback=myFunc"></script>




서버는 JSON 데이터를 함수 호출로 감싸서 응답합니다.

1
2
// 서버 응답 (JSON이 아니라 JavaScript)
myFunc({ "name": "jun", "age": 30 });




브라우저 입장에서는 그냥 <script> 태그로 JS 파일을 실행하는 것이므로 Cross-Origin 제한에 걸리지 않습니다. 클라이언트가 미리 myFunc을 정의해두면 데이터를 받을 수 있었습니다. 하지만 JSONP는

1
2
3
function myFunc(data) {
  console.log(data.name); // "jun"
}





4. SDK 설계 시 고려해야 할 보안 문제

SDK 설계 시, 고려해야 할 보안 요소도 존재하는데요. 이를 간단히 살펴보겠습니다.

  1. API Key 노출
  2. 도메인 제한
  3. Rate Limit
  4. Preflight 발생



**4-1. API Key 노출 **

API Key는 클라이언트 코드에 포함됩니다. 브라우저 DevTools에서 누구나 볼 수 있습니다. API Key가 노출되는 것 자체는 막을 수 없습니다. 클라이언트에서 실행되는 코드에 포함된 이상 당연한 결과입니다. 그래서 API Key만으로 인증을 완료하면 안 되고, 추가적인 보안 장치가 필요합니다.

1
2
# DevTools > Network 탭에서 바로 보임
X-API-Key: CUSTOMER_API_KEY




4-2. 도메인 제한

가장 기본적인 방어는 API Key에 허용된 도메인을 등록하는 것입니다. 서버에서 요청의 Origin 헤더를 확인해서, 등록되지 않은 도메인에서 온 요청은 거부합니다. Google Maps API, Facebook SDK 등 대부분의 클라이언트 SDK가 이 방식을 사용합니다.

1
2
3
4
5
6
api_key = request.headers.get("X-API-Key")
origin = request.headers.get("Origin")
tenant = await find_tenant_by_key(api_key)

if origin not in tenant.allowed_origins:
    return 403 Forbidden




4-3. Rate Limit

도메인 제한만으로는 부족합니다. 허용된 도메인에서라도 비정상적으로 많은 요청이 올 수 있습니다. 테넌트별 Rate Limit을 걸어서 과도한 사용을 방지합니다.

1
2
3
4
5
6
# 테넌트별 요청 제한
tenant_id: "customer_123"
│
├─ 분당 최대 100 요청
├─ 일일 최대 10,000 요청
└─ 초과 시 → 429 Too Many Requests




4-4. Preflight 발생

위젯에서 fetch( )로 요청을 보낼 때, 항상 바로 본 요청이 가는 것은 아닙니다. 커스텀 헤더(X-API-Key)를 포함하면 브라우저는 먼저 OPTIONS 메서드로 Preflight 요청을 보내서 서버에 허락을 구합니다. Preflight가 발생하는 조건은 다음과 같습니다. 우리 위젯은 X-API-Key 커스텀 헤더를 사용하므로 모든 요청에 Preflight가 발생합니다. Access-Control-Max-Age: 86400을 설정하면 브라우저가 24시간 동안 Preflight 결과를 캐시해서 매번 보내지 않습니다.

  • GET, HEAD, POST 이외의 HTTP 메서드 사용
  • Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 이외인 경우
  • 커스텀 헤더 포함 (예: X-API-Key)





5. 참조



This post is licensed under CC BY 4.0 by the author.