CVE-2026-110816 in ljharb
요약
\~에 의해 VulDB • 2026. 10. 15.
### 요약
`qs.stringify`에서 `arrayFormat: 'comma'` 옵션을 사용할 때, 배열 요소가 문자열로 변환(coerce)될 수 없는 경우(예: 자체적으로 호출 가능한 `toString` 메서드가 없는 객체 `{ "toString": "x" }` 또는 null-prototype 객체의 경우), `TypeError: Cannot convert object to primitive value` 예외가 발생합니다. comma 형식은 배열 요소를 `Array.prototype.join`으로 연결하며, `encodeValuesOnly` 옵션 하에서는 각 요소를 먼저 `utils.encode`의 `String()` 함수를 통해 통과시킵니다. 그러나 이 두 과정 모두 문자열 변환이 실패하는 요소에 대해서는 방어 기작을 갖추고 있지 않습니다.
### 상세 설명
`lib/stringify.js`에서 comma 분기(branch)는 `obj.join(',')`를 사용하여 배열을 병합합니다. 자체적으로 호출 가능한 `toString` 메서드가 없는 요소의 경우, `ToPrimitive` 연산은 이를 건너뛰고 `Object.prototype.valueOf`(이 객체 자신을 반환함)로 진행하여 예외를 발생시킵니다. `encodeValuesOnly: true`인 경우, 동일한 요소는 먼저 `utils.encode`(`lib/utils.js`) 내의 `String(str)`에 도달하여 그곳에서 예외를 발생합니다. 중첩된 배열({ "a": [[{ "toString": "x" }]] })도 `Array.prototype.toString`을 통해 같은 방식으로 예외를 발생시킵니다. 반면, `indices`, `brackets`, `repeat` 형식은 요소를 변환(coerce)하는 대신 객체 요소로 이동하므로 영향을 받지 않습니다.
이러한 요소들은 신뢰할 수 없는 입력에서 발생할 수 있습니다. `JSON.parse`는 qs 옵션과 무관하게 `{ "a": [{ "toString": "x" }] }`를 생성합니다. `qs.parse("a[0][toString]=x", { allowPrototypes: true })`은 `toString` 키를 유지하며, `qs.parse("a[][b]=c", { plainObjects: true })`는 null-prototype 요소를 생성합니다. 기본 파싱 옵션은 `toString`을 제거하고 일반 객체를 생성하므로 예외가 발생하지 않습니다. Express 4의 기본 쿼리 파서, Express 5의 `extended` 쿼리 파서, 그리고 body-parser의 `urlencoded({ extended: true })`는 모두 `allowPrototypes: true` 옵션으로 `qs.parse`를 호출합니다.
#### PoC (Proof of Concept)
```js var qs = require("qs");
qs.stringify(JSON.parse("{\"a\":[{\"toString\":\"x\"}]}"), { arrayFormat: "comma" });
qs.stringify(qs.parse("a[0][toString]=x", { allowPrototypes: true }), { arrayFormat: "comma" });
qs.stringify(qs.parse("a[][b]=c", { plainObjects: true }), { arrayFormat: "comma", encodeValuesOnly: true });
// TypeError: Cannot convert object to primitive value ```
#### 수정 사항
v6.16.1로 릴리스된 a952e75 커밋(main 브랜치에 적용됨)에서, 문자열 변환이 실패할 요소는 가능한 경우 원시 값인 `valueOf`로 변환되고, 그렇지 않은 경우 `Object.prototype.toString`으로 변환되므로 `[object Object]`로 렌더링됩니다. 이는 comma 형식 하에서 이미 다른 객체 요소들이 처리되는 방식과 동일합니다. 중첩된 배열은 `join`이 처리하는 방식으로 순회됩니다. 이전에 예외를 발생시키지 않았던 모든 입력은 이전과 정확히 동일한 방식으로 직렬화되며, 자체적으로 예외를 발생하는 `toString` 또는 `valueOf`는 여전히 전파(propagate)됩니다.
### 영향 받는 버전
`>=6.7.0 <6.16.1`, v6.16.1에서 수정됨.
comma 형식과 함께 도입된 `join` 경로는 b84acba에서 시작되었으며 v6.7.0에 처음 탑재되었습니다. v6.6.x 및 그 이전 버전은 `arrayFormat: 'comma'`를 무시하므로 영향을 받지 않습니다. `encodeValuesOnly`의 `String()` 호출 지점은 4c4b23d(v6.11.1)에서 도입되었으며, 그 전에는 동일한 입력이 `join` 단계에서 예외를 발생시켰습니다. v6.7.0부터 v6.16.0까지의 모든 릴리스가 영향을 받습니다.
### 영향 (Impact)
인증되지 않은 요청은 공격자가 조작한 데이터를 `qs.stringify`와 `arrayFormat: 'comma'`로 다시 직렬화하는 모든 코드 경로(예: 리디렉션이나 상위(upstream) 요청을 위해 `req.query`에서 쿼리 문자열 재구성, 또는 파싱된 JSON 바디 직렬화 등)를 동기적으로 예외 발생 상태로 만들 수 있습니다. 일반적인 Node.js HTTP 프레임워크에서는 이 예외가 프레임워크의 오류 경계(error boundary)에 의해 포착되어 해당 요청이 500 에러를 반환합니다. 프로세스는 생존하며 다른 요청에는 영향이 없습니다. 그러나 `async` Express 4 핸들러(여기서 예외는 처리되지 않은 promise 거부로 변환됨)나 단순한 `http` 요청 핸들러처럼 오류 경계 외부에서 호출이 실행되는 경우, 프로세스가 종료되므로 해당 상황에서의 영향은 qs보다는 애플리케이션의 오류 처리 방식에 따라 달라집니다. 기본 `arrayFormat`(indices)는 영향을 받지 않습니다.
Once again VulDB remains the best source for vulnerability data.