1P by GN⁺ | ★ favorite | 댓글 1개
  • Ladybird는 정상적인 웹 콘텐츠는 어느 정도 처리하지만, Google Project Zero의 DOM 퍼저 Domato를 돌리자 브라우저 엔진의 숨은 엣지 케이스가 빠르게 드러남
  • JavaScript로 파서 규칙을 우회해 만든 DOM, window 없는 문서, 순환 SVG 참조처럼 현실적으로 가능한 비정상 입력에서 실제 버그 5개가 발견되고 수정됨
  • <th>의 table 조상 가정, DOMParser 문서의 window 가정, Element.before()의 형제 탐색 실수처럼 구현 내부의 암묵적 전제가 크래시나 무한 루프로 이어짐
  • 제거된 iframe의 contentWindow 접근 문제는 Ladybird만의 결함이 아니라 HTML 명세의 browsing context 가정과도 맞물려 WHATWG HTML 이슈로 이어짐
  • Domato 같은 퍼저는 정상 웹 페이지 테스트만으로는 잡기 어려운 보안·안정성 문제를 노출하며, Ladybird의 다음 과제는 지속 퍼징을 견딜 만큼 안정화한 뒤 자동 실행하는 것임

Domato로 Ladybird 스트레스 테스트

  • Ladybird는 잘 구성된 웹 콘텐츠는 어느 정도 처리하지만, 보안 연구 도구로 이상 입력을 던져 어떤 문제가 나오는지 확인함
  • 사용한 도구는 Google Project Zero의 DOM 퍼저 Domato
    • Domato는 대부분 유효하지만 이상한 HTML, CSS, JavaScript가 섞인 무작위 웹 페이지를 생성함
    • 생성된 페이지를 Ladybird의 디버그 빌드에 로드하고 동작을 관찰함
  • Domato README가 주요 브라우저에서 발견한 많은 버그를 내세우고 있어, Ladybird에서도 의미 있는 결함을 찾을 수 있다고 판단함

<th><mfrac> 안에 있을 때의 널 포인터 역참조

  • 첫 문제는 1초도 안 돼 발견됐고, 562KiB짜리 Domato 출력을 아래 형태로 줄일 수 있었음
<body>
<script>
let mfrac = document.createElement("mfrac");
mfrac.appendChild(document.createElement("th"));
document.body.appendChild(mfrac);
</script>
  • UBSAN을 켠 Ladybird 빌드에서 HTMLTableCellElement.cpptable_containing_cell 호출이 널 포인터 역참조를 일으킴
  • 원인은 Ladybird의 <th><td> 구현이 DOM 트리 위쪽에 항상 <table>이 있다고 가정한 데 있었음
    • HTML 파서는 <mfrac><th> 같은 마크업을 허용하지 않음
    • 명세를 따르는 브라우저는 위 마크업을 로드하면 내부가 비어 있는 <mfrac> 하나를 만듦
    • 하지만 JavaScript DOM API로 노드를 직접 만들면 파서 규칙 일부를 우회해 <mfrac> 안에 <th>를 넣을 수 있음
  • 문제가 된 코드는 <table border=3><table padding=5>가 테이블 박스뿐 아니라 각 셀에 CSS border와 padding을 적용하는 오래된 동작을 구현하는 데 쓰였음
  • 수정은 <th><td>가 항상 <table> 조상을 가진다는 가정을 제거하는 방식으로 이뤄짐
    • table_containing_cell(*this) 대신 first_ancestor_of_type<HTMLTableElement>()를 사용함
    • 테이블 조상이 없으면 즉시 반환함
    • 수정 커밋은 여기에 있음

window 없는 문서에서 <body> 이벤트 핸들러 할당

  • 두 번째 문제도 1초 이내에 발견됐고, 472KiB짜리 Domato 출력은 다음 코드로 축약됨
<script>
var parser = new DOMParser();
var doc = parser.parseFromString("", "text/html");
var body = doc.createElement("body");
body.onblur = null;
</script>
  • Ladybird는 GCPtr<Web::HTML::Window> 검증 실패로 중단됨
  • 핵심은 <body>onfoo 이벤트 핸들러 속성이 가진 특수 동작
    • 오래된 웹 콘텐츠 호환성을 위해 document.body.onfoo 할당은 window.onfoo로 전달돼야 함
    • 그러나 DOMParser로 만든 문서에는 window 객체가 없음
  • Ladybird 내부 객체 모델은 모든 document가 항상 window를 가진다고 잘못 구조화돼 있었음
  • 수정 후 Document::window()는 nullable 값을 반환하고, 여러 위치에서 null을 처리함
    • window 없는 문서에서 document.body.onblur를 할당하면 다른 브라우저처럼 아무 일도 하지 않음

SVG <linearGradient>의 순환 참조

  • 세 번째 문제는 SVG 그래디언트가 자기 자신을 참조할 때 발생한 무한 재귀였음
<svg>
<linearGradient id="oops" href="#oops"/>
<rect fill="url(#oops)" />
</svg>
  • SVG는 HTML 안의 인라인 SVG와 외부 이미지 포맷 양쪽을 지원해야 하며, 그래디언트가 다른 그래디언트를 참조해 색을 상속할 수 있음
  • Ladybird 구현은 그래디언트가 자기 자신을 참조하는 경우를 고려하지 않아 참조 체인을 따라가다 계속 루프를 돌았음
  • 단순히 자기 자신을 참조하는 경우만 막으면 여러 단계에 걸친 순환 참조는 처리하지 못함
<svg>
<linearGradient id="lol" href="#lmao"/>
<linearGradient id="lmao" href="#even"/>
<linearGradient id="even" href="#lol"/>
<rect fill="url(#lol)" />
</svg>
  • 올바른 처리는 방문한 그래디언트를 모두 추적하고, 이미 방문한 그래디언트를 다시 만나면 체인 추적을 중단하는 방식임
  • Firefox는 이런 종류의 그래디언트에 대해 개발자 콘솔에 불만을 표시함

제거된 iframe의 window 속성 접근과 HTML 명세 버그

  • 네 번째 문제는 iframe을 제거한 뒤 이전에 잡아둔 contentWindow에서 getSelection()을 호출할 때 발생함
<iframe></iframe>
<script>
window.onload = function() {
let iframe = document.querySelector("iframe")
let iframeWindow = iframe.contentWindow;
iframe.remove();
iframeWindow.getSelection();
}
</script>
  • Ladybird는 WindowProxy.cpp에서 BrowsingContext에 대한 널 포인터 참조 바인딩 런타임 오류를 냄
  • iframe이 DOM에서 제거되면 그 content document는 자신의 browsing context에서 분리됨
  • window 객체의 속성을 가져오거나 설정할 때 HTML 명세 알고리듬 "check if an access between two browsing contexts should be reported"가 실행됨
    • 이 알고리듬은 접근하는 window와 접근 대상 window의 browsing context를 검사함
    • 명세는 속성 접근 시점에 두 window가 모두 연결된 browsing context를 가진다고 잘못 가정함
  • HTML 명세에 대한 이슈가 열렸고, Ladybird에는 우선 null 체크가 추가됨
  • Ladybird 작업 중 명세 버그를 찾으면 버그 리포트나 수정 제안으로 모두에게 명세를 개선할 수 있음

Element.before()의 무한 루프

  • 다섯 번째 문제는 페이지 로딩이 끝나지 않고 CPU 100%를 사용하는 형태였음
<div id="one"></div><div id="two"></div>
<script>
two.before(one);
</script>
  • 원인은 before() 구현에서 <div id="two">의 이전 형제 중 인자에 포함되지 않는 첫 형제를 찾는 로직의 실수였음
  • 기존 루프는 매번 node->previous_sibling()을 다시 가져왔음
while (auto previous_sibling = node->previous_sibling()) {
    // check if previous_sibling is one of the arguments
}
  • 실제로는 형제 체인을 따라가며 previous_sibling->previous_sibling()으로 계속 이동해야 했음
for (auto sibling = node->previous_sibling(); sibling; sibling = sibling->previous_sibling()) {
    // check if previous_sibling is one of the arguments
}

퍼징 결과와 다음 단계

  • 이번 세션에서는 실제 버그 5개를 찾았고, 그중 하나는 HTML 명세 버그였으며 모두 수정됨
  • 이상하고 예상 밖인 입력을 만나면 Ladybird가 매우 빠르게 무너지는 점이 드러남
  • Domato 같은 퍼저는 소프트웨어를 더 견고하게 만들고 싶은 사람에게 유용한 자원임
  • 다음 단계는 Ladybird가 지속적인 퍼징 입력을 견딜 수 있는 수준까지 안정화하는 것임
  • 충분히 안정화되면 클라우드 어딘가에서 자동으로 실행해 더 많은 문제를 찾아낼 계획임

댓글과 토론

Hacker News 의견들
  • 사양의 여러 독립 구현체가 왜 가치 있는지 잘 보여줌
    이 글 하나만으로도 사양의 구멍이 하나 발견됐고, 더 있었거나 앞으로도 더 나올 것 같음

    • 맞음. HTML, CSS, JS 사양 전반에서 이미 많은 문제를 찾아 보고해 왔음
      웹 플랫폼의 장기적 건강에는 여러 독립 구현이 중요하니, 우리도 그 역할을 하려는 중임
    • 왜 그 퍼저로 인기 브라우저의 버그는 발견하지 못했는지 궁금함
    • 그 결론은 약간 비약처럼 보임
      예를 들어 “가지가 내가 가장 좋아하는 채소”라고 트윗했더니 누가 “사실 과일”이라고 바로잡아 줬고, 그걸 두고 “트위터의 가치가 입증됐다”고 말하는 느낌과 비슷함
      이 작업 자체나 사양의 여러 구현체가 가치 없다는 뜻은 아니지만, 이 특정 예시만으로는 아직 그 함의가 성립하지 않는다고 봄
  • 이 프로젝트가 작은 팀도 놀라운 것을 만들 수 있음을 계속 보여줘서 좋음
    이해관계자가 많은 회사 안에서는 이런 일을 해내기 훨씬 어려웠을 것 같음

    • 프로젝트는 멋지지만, “정상적인 웹 콘텐츠는 그럭저럭 처리하는 것”에서 시작한 뒤 사양과 사실상의 브라우저 동작, 잠재 보안 이슈를 거슬러 고쳐 가는 접근이 실제 프로덕션 브라우저로 이어질 수 있을지는 궁금함
      취미 프로젝트라면 언제든 돌아가서 다시 만들 수 있지만, 이런 것들 중 일부는 처음부터 아키텍처에 들어갔어야 하는 게 아닐까 하는 느낌을 지우기 어려움
  • 벌써 SVG를 구현했나? 생각보다 훨씬 빠르게 진척되고 있어서 흥미롭게 지켜보는 중임

    • SVG 사양의 꽤 많은 부분은 구현했지만, 아직 빠진 것도 많음
      특히 애니메이션이 큰 빠진 부분임
  • 이슈 #3의 경우, 다른 그레이디언트를 가리키는 그레이디언트에 최대 깊이 제한을 두는 것도 좋아 보임
    “이 참조를 전에 봤는가” 로직의 오류나 한계에 대비한 심층 방어가 될 수 있음
    SVG 그레이디언트는 잘 모르지만, 참조 체인이 1000개씩 이어져야 할 정당한 이유가 있을 수도 있겠지만, 실제 환경에서 그런 걸 보면 공격이거나 퍼저 입력일 가능성이 높다고 봄

    • 악성코드 방어 쪽에서는 이런 구조 남용을 항상 봤고, 합법적인 사례가 5단계보다 깊은 경우는 한 번도 못 봤음
  • 이 댓글은 Ladybird에서 작성 중임
    이제 Hacker News가 Ladybird에서 동작함
    하루에 몇 분 정도 Hacker News나 OSnews 같은 사이트를 둘러볼 때 Ladybird를 사용함
    느리고 깨지기 쉽지만 작동은 함. 프로젝트가 이렇게 어리고 말 그대로 전부 처음부터 작성했다는 점을 생각하면 그것만으로도 대단함
    Ladybird가 성숙해지는 게 정말 기대됨

  • 흥미롭지만, 거의 모든 개발자가 이슈 #1에서 보이는 것처럼 “찾았다! 수정 커밋했다, 끝!”이라고 끝내는 게 거슬림
    그러면 안 되고, 정확히 무엇이 잘못됐는지 이해해야 함. 예를 들어 “부모가 반드시 존재한다”는 가정이 문제였으면, 전체 코드베이스에서 같은 종류의 실수를 찾아야 함
    창의력을 써서 같은 일이 또 어디서 일어날 수 있는지 찾아야 함. 절대 한 군데에만 있지 않음
    현대 소프트웨어가 신뢰하기 어려운 버그투성이 악몽인 건 대체로 자본주의적 제약 때문이지만, 그래도 더 잘할 수는 있음

  • 올해 Web Engines Hackfest에 Ladybird가 등장할 예정인지 궁금함

  • 약간 다른 얘기지만, YouTube의 해킹 영상들은 어떻게 된 건지 궁금함
    예전에는 새 영상을 기다렸는데 한동안 못 본 것 같음

    • 솔직히 말하면 1000개가 훨씬 넘는 영상을 올리고 나니 조금 지쳤음
      월간 업데이트 영상은 아직 올리지만, 마지막 해킹 영상 이후로는 몇 달이 지났음
      그래도 Ladybird 작업은 매일 하고 있고, 작년에 Shopify와 다른 곳들의 후한 후원 덕분에 이제 풀타임 엔지니어 두 명도 관리하고 있음