mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-09-21 13:31:42 +00:00
fix(worker): clamp RAR header re-read to 8MB to bound corrupt-archive reads
Add MAX_RAR_HEADER_BYTES constant to prevent unbounded ranged reads when a RAR block's HeaderSize is bogus. Real RAR block headers are far smaller; this guards against amplification attacks on corrupt/desynced archives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,32 @@ describe("walkRarVolume", () => {
|
|||||||
// First region starts right after the 8-byte signature
|
// First region starts right after the 8-byte signature
|
||||||
expect(regions![0].offset).toBe(8);
|
expect(regions![0].offset).toBe(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns null when a block claims an absurd header size (corrupt/desynced)", async () => {
|
||||||
|
// RAR5 block with HeaderSize vint encoding a value > 8MB.
|
||||||
|
// Encode 9_000_000 as RAR vint: bytes little-endian 7-bit groups with continuation bit.
|
||||||
|
function encodeVint(n: number): number[] {
|
||||||
|
const out: number[] = [];
|
||||||
|
while (n >= 0x80) {
|
||||||
|
out.push((n & 0x7f) | 0x80);
|
||||||
|
n = Math.floor(n / 128);
|
||||||
|
}
|
||||||
|
out.push(n);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const sig = Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00]); // RAR5 signature
|
||||||
|
const hsVint = encodeVint(9_000_000);
|
||||||
|
// Block = CRC(4) + HeaderSize vint(9MB) + Type(1 byte) + Flags(1 byte)
|
||||||
|
const block = Buffer.concat([Buffer.alloc(4), Buffer.from(hsVint), Buffer.from([0x02, 0x00])]);
|
||||||
|
const vol = Buffer.concat([sig, block]);
|
||||||
|
const size = 20 * 1024 * 1024;
|
||||||
|
const read = async (_id: string, offset: number, length: number) => {
|
||||||
|
if (offset >= vol.length) return Buffer.alloc(0);
|
||||||
|
return vol.subarray(offset, Math.min(offset + length, vol.length));
|
||||||
|
};
|
||||||
|
const regions = await walkRarVolume(read, { fileId: "1", fileSize: BigInt(size), fileName: "c.rar" }, 5, 8);
|
||||||
|
expect(regions).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("readRarListingRanged (single part)", () => {
|
describe("readRarListingRanged (single part)", () => {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import { childLogger } from "../../util/logger.js";
|
|||||||
|
|
||||||
const rlog = childLogger("rar-ranged");
|
const rlog = childLogger("rar-ranged");
|
||||||
const MAX_RAR_BLOCKS = 50000;
|
const MAX_RAR_BLOCKS = 50000;
|
||||||
|
const MAX_RAR_HEADER_BYTES = 8 * 1024 * 1024; // 8 MB — real RAR block headers are far smaller; guards against a corrupt/desynced HeaderSize
|
||||||
const HEADER_CHUNK = 8192;
|
const HEADER_CHUNK = 8192;
|
||||||
|
|
||||||
export async function walkRarVolume(
|
export async function walkRarVolume(
|
||||||
@@ -72,6 +73,7 @@ export async function walkRarVolume(
|
|||||||
const chunkLen = Math.min(HEADER_CHUNK, size - pos);
|
const chunkLen = Math.min(HEADER_CHUNK, size - pos);
|
||||||
let chunk = await read(part.fileId, pos, chunkLen, part.fileSize);
|
let chunk = await read(part.fileId, pos, chunkLen, part.fileSize);
|
||||||
const ext = version === 5 ? parseRar5BlockExtent(chunk, 0) : parseRar4BlockExtent(chunk, 0);
|
const ext = version === 5 ? parseRar5BlockExtent(chunk, 0) : parseRar4BlockExtent(chunk, 0);
|
||||||
|
if (ext.headerBytes > MAX_RAR_HEADER_BYTES) return null;
|
||||||
// Ensure we have the full header bytes to harvest (long filenames).
|
// Ensure we have the full header bytes to harvest (long filenames).
|
||||||
let headerBuf = chunk;
|
let headerBuf = chunk;
|
||||||
if (ext.headerBytes > chunk.length) {
|
if (ext.headerBytes > chunk.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user