🇰🇷 한글 설명
qs 라이브러리에서 arrayFormat 'comma'와 encodeValuesOnly true 옵션을 함께 사용할 때 배열의 null/undefined 요소가 인코더 함수의 length 속성 접근으로 동기적 TypeError를 발생시키는 취약점입니다. skipNulls와 strictNullHandling 옵션이 작동하지 않아 예외 처리 경계 밖의 코드에서 프로세스 크래시를 유발할 수 있습니다.
💡 쉬운 설명
qs 라이브러리의 특정 설정에서 배열에 빈 값(null)이 있으면 자동으로 충돌합니다. 웹 애플리케이션에서 특정 요청이 들어올 때 500 에러를 반환하거나 백그라운드 작업이 실패할 수 있습니다.
🇺🇸 원문 (English)
### Summary
`qs.stringify` throws `TypeError` when called with `arrayFormat: 'comma'` and `encodeValuesOnly: true` on an array containing `null` or `undefined`. The throw is synchronous and not handled by any of qs's null-related options (`skipNulls`, `strictNullHandling`).
### Details
In the comma + `encodeValuesOnly` branch, `lib/stringify.js:145` mapped the array through the raw encoder before joining:
```js
obj = utils.maybeMap(obj, encoder);
```
`utils.encode` (`lib/utils.js:195`) reads `str.length` with no null guard, so a `null` or `undefined` element throws `TypeError`. `skipNulls` and `strictNullHandling` are both checked in the per-element loop below this line and never get a chance to run.
Same class of bug as the filter-array path fixed in 0c180a4. The vulnerable shape of the comma + `encodeValuesOnly` branch was introduced in 4c4b23d ("encode comma values more consistently", PR #463, 2023-01-19), first released in v6.11.1.
#### PoC
```js
const qs = require('qs');
qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true });
qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true });
qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true });
// TypeError: Cannot read properties of null (reading 'length')
// at encode (lib/utils.js:195:13)
// at Object.maybeMap (lib/utils.js:322:37)
// at stringify (lib/stringify.js:145:25)
```
#### Fix
`lib/stringify.js:145`, applied in 21f80b3 on `main` and released as v6.15.2:
```diff
- obj = utils.maybeMap(obj, encoder);
+ obj = utils.maybeMap(obj, function (v) {
+ return v == null ? v : encoder(v);
+ });
```
`null` and `undefined` now pass through `maybeMap` unchanged and reach the `join(',')` step as-is. For `{ a: [null, 'b'] }` this produces `a=,b`, matching the non-`encodeValuesOnly` comma path (which already joins before encoding and produces `a=%2Cb` for the same input). Single-element `[null]` arrays still collapse via the existing `obj.join(',') || null` and remain subject to `skipNulls` / `strictNullHandling` in the main loop.
### Affected versions
`>=6.11.1 <6.15.2` — fixed in v6.15.2.
The vulnerable code shape was introduced in 4c4b23d and first shipped in v6.11.1. Earlier versions — including all of 6.7.x, 6.8.x, 6.9.x, 6.10.x, and 6.11.0 — implemented the comma + `encodeValuesOnly` path differently (joining before encoding) and are not affected. Empirically verified across released versions.
### Impact
Application code that calls `qs.stringify` with both `arrayFormat: 'comma'` and `encodeValuesOnly: true` (both non-default) on input that may contain a `null` or `undefined` array element will throw synchronously instead of producing a query string. In a typical Node.js HTTP framework (Express, Fastify, Koa, hapi) the sync throw is caught by the framework's error boundary and the affected request returns a 500; the worker process does not exit and subsequent requests are unaffected. The "kills the worker process" framing applies only to call sites outside a request-handler error boundary (background jobs, startup paths, stream pipelines) or to deployments with framework error handling explicitly disabled.
The vulnerable input is a `null` or `undefined` entry inside an array; this is reachable from JSON request bodies or from application code constructing arrays from user input, but not from standard HTML form submissions (which produce strings or omitted fields, not literal `null`).
📍 영향받는 시스템
qs 패키지 6.11.1 이상 6.15.2 미만 (6.11.1-6.15.1)
⚔️ 공격 벡터
null/undefined를 포함한 JSON 배열을 HTTP 요청 본문으로 전송하여 취약한 버전의 qs를 사용하는 애플리케이션 서버에서 TypeError 유발
🛡️ 대응 방법
npm install qs@>=6.15.2로 업그레이드 또는 arrayFormat과 encodeValuesOnly 옵션 동시 사용 제거
🔧 검증 코드
출력된 버전이 6.15.2 이상이면 안전, 6.11.1-6.15.1 범위이면 취약함
npm list qs 2>/dev/null | grep qs@