Repository: ktorio/ktor
File: ktor-http/ktor-http-cio/common/src/io/ktor/http/cio/ChunkedTransferEncoding.kt (Kotlin)

Code before:
                    return result
                }
                QUOTE -> inQuotes = !inQuotes
                SEMICOLON -> inExtension = true
                else -> {
                    if (inExtension) continue // always ignore extensions
                    val intValue = byte.toInt() and 0xffff
                    val digit = if (intValue < 0xff) HexTable[intValue] else -1L
                    if (digit == -1L) throw IOException("Invalid chunk size character: 0x${intValue.toString(16)}")
                    result = (result shl 4) or digit
                }
            }
        } finally {
            afterCr = byte == CR
        }
    }
    throw IOException("Chunk size limit exceeded")
}

/**
 * Encoder job type
 *
 * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.http.cio.EncoderJob)
 */
public typealias EncoderJob = ReaderJob

/**
 * Start chunked stream encoding coroutine
 *
 * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.http.cio.encodeChunked)
 */
@OptIn(DelicateCoroutinesApi::class)
public fun encodeChunked(
    output: ByteWriteChannel,
    coroutineContext: CoroutineContext
): EncoderJob = GlobalScope.reader(coroutineContext, autoFlush = false) {
    encodeChunked(output, channel)
}

/**
 * Chunked stream encoding loop
 *
 * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.http.cio.encodeChunked)
 */
public suspend fun encodeChunked(output: ByteWriteChannel, input: ByteReadChannel) {
    try {
        while (!input.isClosedForRead) {
            input.read { source, startIndex, endIndex ->
                if (endIndex == startIndex) return@read 0
                output.writeChunk(source, startIndex, endIndex)
            }

Review comment (suggestion):
When inQuotes is true and CRLF is encountered, the continue statement at line 106 causes the parser to skip over the CRLF without returning. This means if quotes are not properly closed before the CRLF, the parser will continue consuming bytes from the actual chunk data, looking for a closing quote. This breaks the chunked encoding format and could lead to reading chunk data as part of the chunk size header. Add a check before line 106 to ensure we're not trying to continue past a CRLF while still in quotes.
```suggestion
        if (inQuotes && byte != QUOTE) {
            if (byte == CR || byte == LF) {
                throw IOException("Illegal newline character in quoted chunk extension")
            }
            continue
        }
```

Code after:
 *   `LF` can be recognized as a valid line terminator.
 *
 * Security and robustness considerations (see KTOR-9263):
 * - Only standard hexadecimal digits are accepted before the first `;`. Any other character in
 *   this region results in an [IOException].
 * - Chunk extensions and their contents are always ignored for the purpose of computing the size,
 *   but are fully consumed from the stream, including quoted strings, to keep the parser in sync.
 * - A hard upper bound of [MAX_CHUNK_SIZE_LENGTH] bytes is enforced for the entire line to
 *   mitigate resource exhaustion and to reject overly long or maliciously crafted chunk-size
 *   headers.
 *
 * This function is intentionally strict and should be modified with care, as relaxing these
 * checks may reintroduce parsing ambiguities or security vulnerabilities.
 */
@OptIn(InternalAPI::class)
private suspend fun parseChunkSize(input: ByteReadChannel): Long {
    val buffer = input.readBuffer
    var result = 0L
    var inExtension = false
    var inQuotes = false
    var afterCr = false
    var i = 0
    while (i++ < MAX_CHUNK_SIZE_LENGTH) {
        if (buffer.exhausted() && !input.awaitContent()) throw EOFException()
        val byte = buffer.readByte()
        if (inQuotes && byte != QUOTE) continue
        try {
            when (byte) {
                CR -> continue // set in finally block
                LF -> {
                    if (!afterCr) throw IOException("Illegal newline character in chunk size")
                    if (i < 3) throw IOException("Empty chunk size")
                    return result
                }
                QUOTE -> inQuotes = !inQuotes
                SEMICOLON -> {
                    if (i == 1) throw IOException("Empty chunk size")
                    inExtension = true
                }
                else -> {
                    if (inExtension) continue // always ignore extensions
                    val intValue = byte.toInt() and 0xff
                    val digit = if (intValue < 0xff) HexTable[intValue] else -1L
                    if (digit == -1L) throw IOException("Invalid chunk size character: 0x${intValue.toString(16)}")
                    if ((result and -0x1000000000000000L) != 0L) throw IOException("Chunk size overflow")
                    result = (result shl 4) or digit
                }
            }
        } finally {
            afterCr = byte == CR
        }