undici 6.27.0 → 6.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -350,7 +350,13 @@ function processHeader (request, key, val) {
350
350
  } else if (typeof val[i] === 'object') {
351
351
  throw new InvalidArgumentError(`invalid ${key} header`)
352
352
  } else {
353
- arr.push(`${val[i]}`)
353
+ // Coerce primitives (and reject unsafe coercions such as functions
354
+ // with a crafted toString/Symbol.toPrimitive).
355
+ const str = `${val[i]}`
356
+ if (!isValidHeaderValue(str)) {
357
+ throw new InvalidArgumentError(`invalid ${key} header`)
358
+ }
359
+ arr.push(str)
354
360
  }
355
361
  }
356
362
  val = arr
@@ -361,7 +367,12 @@ function processHeader (request, key, val) {
361
367
  } else if (val === null) {
362
368
  val = ''
363
369
  } else {
370
+ // Coerce primitives (and reject unsafe coercions such as functions
371
+ // with a crafted toString/Symbol.toPrimitive).
364
372
  val = `${val}`
373
+ if (!isValidHeaderValue(val)) {
374
+ throw new InvalidArgumentError(`invalid ${key} header`)
375
+ }
365
376
  }
366
377
 
367
378
  if (headerName === 'host') {
@@ -10,6 +10,7 @@ const {
10
10
  RequestContentLengthMismatchError,
11
11
  ResponseContentLengthMismatchError,
12
12
  RequestAbortedError,
13
+ InvalidArgumentError,
13
14
  HeadersTimeoutError,
14
15
  HeadersOverflowError,
15
16
  SocketError,
@@ -993,8 +994,16 @@ function writeH1 (client, request) {
993
994
  }
994
995
  body = bodyStream.stream
995
996
  contentLength = bodyStream.length
996
- } else if (util.isBlobLike(body) && request.contentType == null && body.type) {
997
- headers.push('content-type', body.type)
997
+ } else if (util.isBlobLike(body) && request.contentType == null) {
998
+ const contentType = body.type
999
+ if (contentType) {
1000
+ const contentTypeValue = `${contentType}`
1001
+ if (!util.isValidHeaderValue(contentTypeValue)) {
1002
+ util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
1003
+ return false
1004
+ }
1005
+ headers.push('content-type', contentTypeValue)
1006
+ }
998
1007
  }
999
1008
 
1000
1009
  if (body && typeof body.read === 'function') {
@@ -15,6 +15,28 @@ function calculateRetryAfterHeader (retryAfter) {
15
15
  return new Date(retryAfter).getTime() - current
16
16
  }
17
17
 
18
+ function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
19
+ const contentLength = headers['content-length']
20
+ if (contentLength == null) {
21
+ return null
22
+ }
23
+
24
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
25
+ return null
26
+ }
27
+
28
+ const length = Number(contentLength)
29
+ const expectedLength = range.end - range.start + 1
30
+ if (!Number.isFinite(length) || length !== expectedLength) {
31
+ return new RequestRetryError('Content-Length mismatch', statusCode, {
32
+ headers,
33
+ data: { count: retryCount }
34
+ })
35
+ }
36
+
37
+ return null
38
+ }
39
+
18
40
  class RetryHandler {
19
41
  constructor (opts, handlers) {
20
42
  const { retryOptions, ...dispatchOpts } = opts
@@ -229,6 +251,12 @@ class RetryHandler {
229
251
  return false
230
252
  }
231
253
 
254
+ const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
255
+ if (contentLengthError != null) {
256
+ this.abort(contentLengthError)
257
+ return false
258
+ }
259
+
232
260
  const { start, size, end = size - 1 } = contentRange
233
261
 
234
262
  assert(this.start === start, 'content-range mismatch')
@@ -252,6 +280,12 @@ class RetryHandler {
252
280
  )
253
281
  }
254
282
 
283
+ const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
284
+ if (contentLengthError != null) {
285
+ this.abort(contentLengthError)
286
+ return false
287
+ }
288
+
255
289
  const { start, size, end = size - 1 } = range
256
290
  assert(
257
291
  start != null && Number.isFinite(start),
@@ -105,7 +105,7 @@ function validateCookiePath (path) {
105
105
 
106
106
  if (
107
107
  code < 0x20 || // exclude CTLs (0-31)
108
- code === 0x7F || // DEL
108
+ code > 0x7E || // exclude DEL and non-ascii
109
109
  code === 0x3B // ;
110
110
  ) {
111
111
  throw new Error('Invalid cookie path')
@@ -114,16 +114,80 @@ function validateCookiePath (path) {
114
114
  }
115
115
 
116
116
  /**
117
- * I have no idea why these values aren't allowed to be honest,
118
- * but Deno tests these. - Khafra
117
+ * <let-dig> ::= <letter> | <digit>
118
+ *
119
+ * <letter> ::= any one of the 52 alphabetic characters A through Z in
120
+ * upper case and a through z in lower case
121
+ *
122
+ * <digit> ::= any one of the ten digits 0 through 9r
123
+ *
124
+ * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
125
+ * @param {number} code
126
+ */
127
+ function isLetterOrDigit (code) {
128
+ return (
129
+ (code >= 0x30 && code <= 0x39) || // 0-9
130
+ (code >= 0x41 && code <= 0x5A) || // A-Z
131
+ (code >= 0x61 && code <= 0x7A) // a-z
132
+ )
133
+ }
134
+
135
+ /**
136
+ * Validates a cookie domain against the "preferred name syntax".
137
+ *
138
+ * <domain> ::= <subdomain> | " "
139
+ * <subdomain> ::= <label> | <subdomain> "." <label>
140
+ * <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
141
+ * <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
142
+ * <let-dig-hyp> ::= <let-dig> | "-"
143
+ *
144
+ * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
145
+ * @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
146
+ * @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
119
147
  * @param {string} domain
120
148
  */
121
149
  function validateCookieDomain (domain) {
122
- if (
123
- domain.startsWith('-') ||
124
- domain.endsWith('.') ||
125
- domain.endsWith('-')
126
- ) {
150
+ // <domain> ::= <subdomain> | " "
151
+ if (domain === ' ') {
152
+ return
153
+ }
154
+
155
+ if (domain.length > 255) {
156
+ throw new Error('Invalid cookie domain')
157
+ }
158
+
159
+ let labelLength = 0
160
+
161
+ for (let i = 0; i < domain.length; ++i) {
162
+ const code = domain.charCodeAt(i)
163
+
164
+ if (code === 0x2E) {
165
+ if (labelLength === 0) {
166
+ throw new Error('Invalid cookie domain')
167
+ }
168
+
169
+ if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
170
+ throw new Error('Invalid cookie domain')
171
+ }
172
+
173
+ labelLength = 0
174
+ continue
175
+ }
176
+
177
+ if (labelLength === 0 && !isLetterOrDigit(code)) {
178
+ throw new Error('Invalid cookie domain')
179
+ }
180
+
181
+ if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
182
+ throw new Error('Invalid cookie domain')
183
+ }
184
+
185
+ if (++labelLength > 63) {
186
+ throw new Error('Invalid cookie domain')
187
+ }
188
+ }
189
+
190
+ if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
127
191
  throw new Error('Invalid cookie domain')
128
192
  }
129
193
  }
@@ -266,7 +330,13 @@ function stringify (cookie) {
266
330
 
267
331
  const [key, ...value] = part.split('=')
268
332
 
269
- out.push(`${key.trim()}=${value.join('=')}`)
333
+ const trimmedKey = key.trim()
334
+ const joinedValue = value.join('=')
335
+
336
+ validateCookieName(trimmedKey)
337
+ validateCookieValue(joinedValue)
338
+
339
+ out.push(`${trimmedKey}=${joinedValue}`)
270
340
  }
271
341
 
272
342
  return out.join('; ')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.27.0",
3
+ "version": "6.28.0",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {