Skip to content

Commit c6504d6

Browse files
committed
glyph11: a Content-Length the checks accept frames a length - OWS before a list's comma no longer drops it, and 2^63 and up is held at Long.MAX_VALUE - never no body, which read the body as the next request
1 parent 582fff5 commit c6504d6

4 files changed

Lines changed: 63 additions & 14 deletions

File tree

‎glyph11/src/main/kotlin/ExactParser.kt‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ internal object ExactParser {
277277

278278
/**
279279
* BodyFramingDetector.detectBodyFraming over a parsed [RequestHead]: chunked if a Transfer-Encoding is "chunked",
280-
* else the last Content-Length's leading digits when they are positive, else no body. It keeps the detector's
281-
* reading of the value exactly: the digits before the first comma, so `5 ,5` (a space before the comma) is no body.
280+
* else the last Content-Length's leading digits when they are positive, else no body: the digits before the first
281+
* comma and the OWS before it, so `5 ,5` is 5.
282282
*/
283283
internal object HeadFraming {
284284
private val TE = "transfer-encoding".toByteArray(Charsets.US_ASCII)
@@ -309,7 +309,9 @@ internal object HeadFraming {
309309
}
310310
if (clStart >= 0 && clEnd > clStart) {
311311
val comma = Ascii.indexOf(a, ','.code, clStart, clEnd)
312-
val value = leadingDigits(a, clStart, if (comma >= 0) comma else clEnd)
312+
var to = if (comma >= 0) comma else clEnd
313+
while (to > clStart && isOws(a[to - 1].toInt())) to--
314+
val value = leadingDigits(a, clStart, to)
313315
if (value > 0) {
314316
head.bodyFraming = RequestHead.BODY_LENGTH
315317
head.contentLength = value
@@ -319,7 +321,10 @@ internal object HeadFraming {
319321
head.bodyFraming = RequestHead.BODY_NONE
320322
}
321323

322-
/** `[from, to)` as a decimal number, -1 if it is empty, holds a non-digit, or overflows a Long. */
324+
/**
325+
* `[from, to)` as a decimal number, -1 if it is empty or holds a non-digit, Long.MAX_VALUE from 2^63 up: longer
326+
* than any body may be, so refused rather than read as no body.
327+
*/
323328
@JvmStatic
324329
fun leadingDigits(a: ByteArray, from: Int, to: Int): Long {
325330
if (to == from) return -1
@@ -328,7 +333,10 @@ internal object HeadFraming {
328333
val d = a[j] - '0'.code.toByte()
329334
if (d < 0 || d > 9) return -1
330335
result = result * 10 + d
331-
if (result < 0) return -1
336+
if (result < 0) {
337+
for (k in j + 1 until to) if (a[k] < '0'.code.toByte() || a[k] > '9'.code.toByte()) return -1
338+
return Long.MAX_VALUE
339+
}
332340
}
333341
return result
334342
}

‎glyph11/src/main/kotlin/Glyph11.kt‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -380,23 +380,23 @@ internal object FastParser {
380380

381381
/**
382382
* A Content-Length value `[v, e)`, its leading OWS skipped: [CL_INVALID] unless UltraHardenedParser accepts it,
383-
* 1*DIGIT without a leading zero and at most 2^64 - 1, its comma-separated copies all the same digits; else what
384-
* BodyFramingDetector frames it as, those digits as a Long, -1 (no body) when they overflow one or OWS stands
385-
* between them and the first comma.
383+
* 1*DIGIT without a leading zero and at most 2^64 - 1, its comma-separated copies all the same digits; else those
384+
* digits as a Long, held at Long.MAX_VALUE from 2^63 up.
386385
*/
387386
@Suppress("NOTHING_TO_INLINE")
388387
private inline fun contentLengthOf(a: ByteArray, v: Int, e: Int): Long {
389388
if (v >= e || !isDigit(a[v])) return CL_INVALID
390389
var p = v + 1
391-
// the digits' value as BodyFramingDetector reads them (HeadFraming.leadingDigits): -1 once a step overflows
390+
// a length past 2^63 - 1 is longer than any body may be: held at Long.MAX_VALUE, so it is refused as too large
391+
// rather than framed as no body, which would read the body as the next request
392392
var framing = (a[v] - '0'.code.toByte()).toLong()
393393
if (a[v] == '0'.code.toByte()) {
394394
if (p < e && isDigit(a[p])) return CL_INVALID
395395
} else {
396396
while (p < e && isDigit(a[p])) {
397-
if (framing >= 0) {
397+
if (framing != Long.MAX_VALUE) {
398398
framing = framing * 10 + (a[p] - '0'.code.toByte())
399-
if (framing < 0) framing = -1
399+
if (framing < 0) framing = Long.MAX_VALUE
400400
}
401401
p++
402402
}
@@ -406,7 +406,6 @@ internal object FastParser {
406406
var q = p
407407
while (q < e && isOws(a[q])) q++
408408
if (q < e) {
409-
if (q > p) framing = -1
410409
while (true) {
411410
if (a[q] != ','.code.toByte()) return CL_INVALID
412411
q++

‎glyph11/src/main/kotlin/validation/BodyFramingDetector.kt‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,12 @@ public object BodyFramingDetector {
135135
val span = ByteSpan(input)
136136
var length = span.length
137137

138-
// Handle comma-separated: take first segment
138+
// Handle comma-separated: take first segment, without the OWS before its comma ("5 ,5" is 5)
139139
val comma = span.indexOfByte(','.code, 0, length)
140140
if (comma >= 0)
141141
length = comma
142+
while (length > 0 && (span.at(length - 1) == ' '.code || span.at(length - 1) == '\t'.code))
143+
length--
142144

143145
if (length == 0) return -1
144146

@@ -147,7 +149,11 @@ public object BodyFramingDetector {
147149
val b = span.at(j)
148150
if (b < '0'.code || b > '9'.code) return -1
149151
result = result * 10 + (b - '0'.code)
150-
if (result < 0) return -1 // overflow
152+
// 2^63 and up: longer than any body may be, so refused as too large rather than read as no body
153+
if (result < 0) {
154+
for (k in j + 1 until length) if (span.at(k) < '0'.code || span.at(k) > '9'.code) return -1
155+
return Long.MAX_VALUE
156+
}
151157
}
152158

153159
return result

‎glyph11/src/test/kotlin/Glyph11HardeningTests.kt‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import ktron.glyph11.parser.ParserLimits
66
import ktron.glyph11.parser.ultrahardened.UltraHardenedParser
77
import ktron.glyph11.protocol.BinaryRequest
88
import ktron.glyph11.protocol.BinaryResponse
9+
import ktron.glyph11.validation.BodyFraming
10+
import ktron.glyph11.validation.BodyFramingDetector
911
import ktron.glyph11.validation.ChunkedBodyValidator
1012
import java.nio.ByteBuffer
1113
import kotlin.test.Test
@@ -116,4 +118,38 @@ class Glyph11HardeningTests {
116118
// the validator added the size to its position and overflowed: it took the chunk's data for the next size line
117119
assertEquals(-1, ChunkedBodyValidator.tryValidate(ByteBuffer.wrap(huge)))
118120
}
121+
122+
// ---- a Content-Length the checks accept frames that length: framed as no body, the body would be read as the
123+
// next request ----
124+
125+
// value -> the length it frames
126+
private val listedLengths = listOf(
127+
"5 ,5" to 5L, "5\t, 5" to 5L, "5 , 5 " to 5L, "5 ,5\r\nContent-Length: 5 ,5" to 5L,
128+
"9223372036854775807" to Long.MAX_VALUE,
129+
"9223372036854775808" to Long.MAX_VALUE, "9999999999999999999" to Long.MAX_VALUE,
130+
"18446744073709551615" to Long.MAX_VALUE, "9223372036854775808, 9223372036854775808" to Long.MAX_VALUE,
131+
)
132+
133+
@Test
134+
fun `OWS before a list's comma and lengths of 2^63 and up still frame a length`() {
135+
for ((value, length) in listedLengths) {
136+
val request = bytes("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: $value\r\n\r\n")
137+
val head = RequestHead()
138+
assertEquals(request.size, Glyph11.parse(request, 0, request.size, head), value)
139+
assertEquals(RequestHead.BODY_LENGTH, head.bodyFraming, value)
140+
assertEquals(length, head.contentLength, value)
141+
}
142+
}
143+
144+
@Test
145+
fun `the ByteBuffer API frames them the same`() {
146+
for ((value, length) in listedLengths) {
147+
val request = BinaryRequest()
148+
val bytes = ByteBuffer.wrap(bytes("POST / HTTP/1.1\r\nHost: x\r\nContent-Length: $value\r\n\r\n"))
149+
UltraHardenedParser.tryExtractFullHeader(bytes, request, ParserLimits.Default)
150+
val framing = BodyFramingDetector.detectBodyFraming(request)
151+
assertEquals(BodyFraming.ContentLength, framing.framing, value)
152+
assertEquals(length, framing.contentLength, value)
153+
}
154+
}
119155
}

0 commit comments

Comments
 (0)