T09 · Insecure Skill Coding Practices
Warning
- Location
- utils.js:58
- Finding
- Unbounded PNG Chunk Parsing Enables Denial of Service## Vulnerability Details **File Location**: `utils.js`, lines 58–70 **Vulnerability Type**: Improper validation of attacker-controlled PNG chunk length **Risk Level**: Medium ### Vulnerable Code ```javascript // Read chunk length (4 bytes, big-endian) if (offset + 8 > buffer.length) break; const length = buffer.readUInt32BE(offset); offset += 4; // Read chunk type (4 bytes) const type = buffer.toString('ascii', offset, offset + 4); offset += 4; // Check whether this is a text chunk if (type === 'tEXt' || type === 'iTXt') { // Read the null-terminated keyword let keywordEnd = offset; while (keywordEnd < offset + length && buffer[keywordEnd] !== 0) { keywordEnd++; } ``` ### Technical Analysis The PNG chunk length is read directly from an untrusted file. The parser does not verify that the declared `length` fits within the bytes remaining in the buffer, nor does it impose a reasonable maximum text-chunk size. For a `tEXt` or `iTXt` chunk, the keyword loop is bounded by `offset + length` but not by `buffer.length`. Once `keywordEnd` exceeds the physical buffer, `buffer[keywordEnd]` evaluates to `undefined`, which remains unequal to zero. The loop can therefore continue until it reaches the attacker-controlled declared chunk boundary. A crafted chunk length approaching `0xffffffff` may cause billions of synchronous loop iterations. Because character-card files are loaded using synchronous file access and parsed on the main Node.js thread, exploitation can block the process and deny service to other operations. The parser also advances with `offset += length + 4` without first establishing that the complete chunk data and CRC are present. The primary demonstrated security consequence is CPU exhaustion during keyword scanning. ### Attack Path 1. The attacker constructs a file with a valid eight-byte PNG signature. 2. The file contains a chunk whose type is `tEXt` or `iTXt`. 3. The chunk length field is set to a very large value, such as one near ...[truncated 968 chars]
- Remediation
- ## Remediation Suggestions Validate every chunk before inspecting its content: 1. Calculate the chunk-data start and end with arithmetic that cannot silently exceed the buffer boundary. 2. Require the complete chunk data and four-byte CRC to be present. 3. Reject chunks whose declared size exceeds the remaining bytes. 4. Enforce a conservative maximum size for textual character-card chunks. 5. Bound keyword scanning by both the validated chunk end and `buffer.length`. 6. Require a null terminator before decoding the keyword. 7. Consider validating chunk CRC values and PNG chunk ordering. 8. Apply an overall input-file size limit before reading or parsing the file. 9. Avoid long synchronous parsing on a shared event-loop thread; use asynchronous processing or an isolated worker where practical. Example hardening pattern: ```javascript const MAX_TEXT_CHUNK_SIZE = 10 * 1024 * 1024; if (offset + 8 > buffer.length) { throw new Error('Truncated PNG chunk header'); } const length = buffer.readUInt32BE(offset); offset += 4; const type = buffer.toString('ascii', offset, offset + 4); offset += 4; if (length > MAX_TEXT_CHUNK_SIZE && (type === 'tEXt' || type === 'iTXt')) { throw new Error('PNG text chunk exceeds the permitted size'); } const chunkEnd = offset + length; const crcEnd = chunkEnd + 4; if (!Number.isSafeInteger(chunkEnd) || crcEnd > buffer.length) { throw new Error('PNG chunk length exceeds the available input'); } if (type === 'tEXt' || type === 'iTXt') { let keywordEnd = offset; while (keywordEnd < chunkEnd && buffer[keywordEnd] !== 0) { keywordEnd++; } if (keywordEnd === chunkEnd) { throw new Error('PNG text chunk has no keyword terminator'); } // Continue parsing only within [offset, chunkEnd). } ```
