[기술스터디] Node.js url.parse() 취약점 컨트리뷰션
참고 원문
https://toss.tech/article/nodejs-security-contribution
Node.js url.parse() 취약점 컨트리뷰션
토스 보안기술팀은 안전한 금융 서비스를 제공하기 위한 연구를 수행하고 있어요. 많은 서비스에서 사용되고 있는 Node.js의 취약점을 분석하고 안전하게 패치될 수 있도록 기여했던 과정을 소개
toss.tech
toss tech, 표상영, Node.js url.parse() 취약점 컨트리뷰션, 2023.5.12
Node.js의 Built-in API 중 하나인 url.parse()의 Hostname 스푸핑 취약점을 발견하고 패치하는 과정을 소개
Node.js는 웹 상에서 사용되는 자바스크립트 개발 플랫폼이고, Built-in API는 웹 브라우저에서 사용되는 API를 의미한다.
즉 웹 브라우저에서 자주 인용되는 api 안에 속한 url.parse() 함수의 취약점을 발견하고 패치하는 과정이다.
취약점이 발생한 Node.js의 url api 라이브러리
https://github.com/nodejs/node/blob/v19.0.1/lib/url.js
GitHub - nodejs/node: Node.js JavaScript runtime
Node.js JavaScript runtime :sparkles::turtle::rocket::sparkles: - GitHub - nodejs/node: Node.js JavaScript runtime
github.com
url.parse() 취약점 발생 원인
Node.js에 속한 url.parse()함수는 WHATWG URL API가 아니라 자체적인 스펙으로 개발되었기 때문에 취약점이 발생할 수 있었다. 표준 스펙이 아닌 자체 스펙으로 해석하여 다른 파서(parser : 코드를 읽는 컴파일러 세부 분류 중 웹 브라우저 자바 스크립트를 읽는 것이라고 보면 될듯)들과 해석 결과가 달라지는 것이다.
WHATWG(웹 표준화 그룹, Web Hypertext Application Technology Working Group) URL API
웹 표준화 그룹 WHATWG에서 인정하는 국제 표준 스펙으로, URL을 다룰 수 있도록 제공되는 API를 말한다.
url.parse()에서는 url 파싱을 하는 과정 중 hostname을 잘못된 방식으로 파싱하여 취약점이 발생했다. 아래는 취약점이 발생한 getHostname() 함수이다.
/* comment
해당 취약점은 v19.1.0에서 패치되었습니다.
아래 코드는 v19.1.0 이전 버전에서 확인할 수 있습니다.
*/
function getHostname(self, rest, hostname) {
for (let i = 0; i < hostname.length; ++i) {
const code = hostname.charCodeAt(i);
const isValid = (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) ||
code === CHAR_DOT ||
(code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) ||
(code >= CHAR_0 && code <= CHAR_9) ||
code === CHAR_HYPHEN_MINUS ||
code === CHAR_PLUS ||
code === CHAR_UNDERSCORE ||
code > 127;
// Invalid host character
if (!isValid) {
self.hostname = hostname.slice(0, i);
return `/${hostname.slice(i)}${rest}`;
}
}
return rest;
}
- for문으로 전달된 값의 문자를 하나씩 가져와 조건에 맞는 값을 구하는 함수
- 조건 = 변수 isVaild = /[a-zA-Z0-9\.\-\+_]/u (ECMAScript 기준) -> 이 값을 어떻게 얻은 건지 궁금함!
- 문자가 isValid라는 조건에 충족되지 않으면 해당 문자의 앞 인덱스까지 자르고 해당 범위를 hostname으로 설정한다. 그리고 그 뒤 문자들은 앞에 /를 붙여 경로로 사용한다. -> isValid의 범위를 벗어난 문자가 들어오면 hostname 파싱을 도중에 중단하고 이후 이어지는 문자열은 path로 사용
이렇듯, /[a-zA-Z0-9\.\-\+_]/u의 범위를 벗어난 문자열은 hostname 파싱을 중단하고 path로 사용하기 때문에 Hostname Spoofing 취약점이 발생하게 된다. (원문에서 실제 디버깅하며 isValid를 벗어난 문자열을 입력하여 hostname 파싱을 중단시키는 것을 보여줌) WHATWG URL API를 사용해서 hostname을 파싱할 경우에는, 범위 외의 문자가 들어와도 중간에 파싱이 끊기지 않는다.
Hostname Spoofing
Hostname을 속이는 해킹 기법
그렇다면 왜 isValid를 정의해뒀을까?
해당 취약점은 Node.js의 url 라이브러리에서 hostname을 파싱할 때 isValid라는 범위 변수를 두고 입력을 제한했기 때문에 발생했다. Node.js에서 굳이 isValid라는 특정 범위를 설정한 이유는 RFC3986 문서 중 2.2 Reserved Characters에서 찾을 수 있다.
*RFC3986는 표준 URI 문법을 정의하고 있는 문서이다.
https://www.rfc-editor.org/rfc/rfc3986
RFC 3986: Uniform Resource Identifier (URI): Generic Syntax
www.rfc-editor.org
RFC3986 문서에서 Reserved Characters 대목에서는 url을 구성할 수 있는 문자 중에서 특수 목적을 가지고 미리 예약된 문자를 정의하고 있다. Reserved Characters로 정의된 문자들은 각각의 기능을 가지고 예약되었기 때문에, 사용자의 입력(url 문자열)으로서 임의 사용될 수 없다. 또한 RFC3986에서는 사용해도 괜찮은 일부 특수 문자(Unreserved Characters)도 미리 정의되어 있는데, 해당 문서에서 정의하고 있는 사용 가능 범위가 getHostname() 함수의 isValid 조건과 유사하다.
따라서, isValid는 RFC표준에 따라서 url에 들어왔을 때 특수 기능을 하는 예약된 문자를 제외하기 위해서 정의된 것이다.
취약점 악용 시나리오 : SSRF 공격
원문 내용이 전부 좋음... 원문으로 읽을 것
이렇게 취약점 하나 발견하고, 디버깅으로 확인하고, 악용 시나리오 구성해서 보여주는 것도 좋은 프로젝트 같다.
취약점 패치 컨트리뷰션
해당 글 작성자는 기존 getHostname() 함수가 Unreserved Chracters를 isValid라는 범위를 두고 화이트리스트 처리를 하는 방식 대신, Reserved Chracters를 블랙리스트로 처리하는 방식으로 패치했다.
https://github.com/nodejs/node/pull/45011
url: improve url.parse() compliance with WHATWG URL by Trott · Pull Request #45011 · nodejs/node
Make the url.parse() hostname parsing closer to that of WHATWG URL parsing. This mitigates for cases where hostname spoofing becomes possible if your code checks the hostname using one API but the ...
github.com