DOM Handling with MutationObserver

최근 ZAP은 SPA 기반의 앱을 쉽게 식별하기 위해 Client Side Integration 이란 기능을 추가했습니다. 이 이 때 DOM의 변화를 식별하기 위한 장치로 MutationObserver가 사용되었는데요. 오늘은 MutationObserver가 뭔지 그리고 보안 테스팅 시 어떻게 사용할 수 있을지 이야기해봅니다.

Observers

웹서비스 페이지는 개발자의 의지, 또는 개발자와 의지와 별개로 변화가 종종 발생할 수 있습니다. 이러한 특성을 고려하여 개발자가 변화를 감지하고 대응할 수 있도록 5가지의 내장 인터페이스를 제공하고 있습니다.

MutationObserver

위에 간략하게 설명했지만 MutationObserver는 DOM tree의 변경을 감지합니다.

Constructor

MutationObserver()를 통해 새 객체를 생성하고 이를 리턴합니다. 생성자 호출 시 callback을 인자값으로 전달해야하고 생성된 객체는 DOM 변경 발생 Callback을 호출할 수 있습니다.

// Callback
const callback = (mutationList, observer) => {
  for (const mutation of mutationList) {
    console.log(mutation.type)
    console.log(mutation.taget)
    console.log(mutation.currentTarget)
    if (mutation.type === "childList") {
      console.log("childList is changed");
    } 
  }
};

const observer = new MutationObserver(callback);

observe()

observe() 메서드는 주어진 설정과 일치하는 DOM 변경이 발생할 때 MutationObserver 인스턴스가 자신의 콜백으로 알림을 수신하도록 설정합니다.

const callback = (mutationList, observer) => {/* callbacks.. */}

const observer = MutationObserver(callback)
observer.observe();

Target

observer 호출 Target 전달 시 MutationObserver는 해당 target을 모니터링하게 됩니다.

// Target
var target = document.querySelector('#some-id');
const callback = (mutationList, observer) => {/* callbacks.. */}

const observer = new MutationObserver(callback);
observer.observe(target);

Config

observer 호출 Config 전달 시 MutationObserver는 설정된 값에 따라 모니터링을 시작합니다.

// Target
var target = document.querySelector('#some-id');
const config = { attributes: true, childList: true, subtree: true };
const callback = (mutationList, observer) => {/* callbacks.. */}

const observer = new MutationObserver(callback);
observer.observe(target, config);

disconnect()

disconnect() 메서드는 observe() 메서드 호출 전까지 MutationObserver 인스턴스가 더 이상 DOM 변경을 감지하지 않도록 설정합니다.

const callback =  (mutationList, observer) => {/* callbacks.. */}
observer = MutationObserver()
observer.observe(callback);
// some logic..
observer.disconnect();

takeRecords()

takeRecords() 메서드는 MutationObserver의 알림 큐를 비우고, 큐에서 대기 중이던 알림들을 MutationRecord로 구성된 새로운 배열로 반환합니다.

const callback =  (mutationList, observer) => {/* callbacks.. */}
observer = MutationObserver()
observer.observe(callback);
// some logic..
tasks = observer.takeRecords()
for (const mutation of tasks) {
    // mutation logic..
}

Mutation’s properties

Call의 mutation에서 사용할 수 있는 속성과 메서드는 아래와 같습니다.

Event Method Description
type 이벤트의 유형을 반환합니다. 예: “click”, “hashchange”, 또는 “submit”.
target 이벤트가 전달된 대상 객체를 반환합니다.
currentTarget 현재 호출 중인 이벤트 리스너의 콜백이 실행되는 객체를 반환합니다.
composedPath() 이벤트 경로의 호출 대상 객체를 반환합니다(리스너가 호출될 개체).
eventPhase 이벤트의 단계를 반환합니다.
(NONE, CAPTURING_PHASE, AT_TARGET, BUBBLING_PHASE)
stopPropagation() 트리에서 전파되는 경우 이 메서드를 호출하면 이벤트가 현재 개체 이외의 다른 개체에 도달하는 것을 방지합니다.
stopImmediatePropagation() 현재 실행 중인 이벤트 리스너 이후에 등록된 다른 이벤트 리스너에 이벤트가 도달하는 것을 방지합니다.
bubbles 이벤트가 초기화된 방식에 따라 true 또는 false를 반환합니다. (true=거꾸로 Tree를 통과한 경우)
cancelable 이벤트가 초기화된 방식에 따라 true 또는 false를 반환합니다.
preventDefault() cancelable 속성 값이 true로 설정된 경우에만 호출되며, event를 발생시킨 작업에게 취소해야 함을 신호로 보냅니다.
defaultPrevented preventDefault()가 성공적으로 호출되어 취소를 나타내면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
composed 이벤트가 초기화된 방식에 따라 true 또는 false를 반환합니다. (true=ShadowROot를 통과하여 호출)
isTrusted 이벤트가 사용자 에이전트에 의해 발생되었는지 여부를 반환합니다.
timeStamp 이벤트의 타임스탬프를 발생 기준으로 밀리초 단위로 반환합니다.

For Pentester

DOM의 변경을 감지하는 방법이라 DOM에 관련된 모든 보안 이슈에는 사용해볼 수 있습니다.

XSS

DOM XSS를 찾는 방법에 사용할 수 있습니다. URL Query, hash, localStorage 등 여러 입력에서 들어오는 값이 DOM 트리 구조의 영향을 주는지 찾기 쉽습니다. 다만 이러한 방법은 Eval Villain 이나 dom-invader 등 DOM 관련 취약점을 찾는데 보조할 수 있는 도구가 워낙 잘 되어 있어서 해당 도구를 쓰는게 훨씬 편리합니다.

Communication of components

SPA 등 DOM 변경이 잦은 앱들은 페이지를 렌더링하기 위해 여러가지 정보를 컴포넌트간 통신하며 전달하기도 합니다. 이 때 MutationObserver를 통해 DOM Tree 내 변경을 감지하고 민감한 정보를 찾아내고 XSS 등의 취약점의 리스크를 끌어올릴 수 있습니다.

var target = document.querySelector('#token-handler');
const config = { attributes: true, childList: true, subtree: true };
const callback = (mutationList, observer) => {
    for (const mutation of mutationList) {
    if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
      console.log(`Leaked token: ${targetElement.value}`);
    }
  }
}

const observer = new MutationObserver(callback);
observer.observe(target, config);

Bypass security control

stopPropagation() 등을 통해 이벤트의 전달을 막아 기존에 Javascript로 구현된 보안 로직이 동작하지 않도록 만들 수 있습니다. 일반적으로 Javascirpt 기반 보호 로직은 유저의 통제가 가능한 영역보다 상단에서 로드하는 것이 좋기 떄문에 이미 보안 요소가 로드된 경우 테스트함에 있어 불편함을 만들지만 이벤트 전달 자체를 막는 경우 보호 로직이 동작하지 않을 수 있습니다.

References