From 165c2303322c2cd8a2492084582e2d56d1209ea5 Mon Sep 17 00:00:00 2001 From: godavid Date: Fri, 13 Nov 2026 21:49:57 +0100 Subject: [PATCH 1/3] =?UTF-8?q?fix(zip):=20az=20=C3=BCtk=C3=B6z=C5=91=20?= =?UTF-8?q?=C3=BAtvonalak=20ki=C3=ADr=C3=A1s=20el=C5=91tti=20elutas=C3=ADt?= =?UTF-8?q?=C3=A1sa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #7 Tested: ugyanaz a szintetikus sorrendteszt a javítás előtt 10 esetben bukott; javítva az extract spec 31/31 zöld. Nincs portál- vagy ügyféladat-próba. --- src/extract.ts | 12 ++++++++++++ test/extract.test.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/extract.ts b/src/extract.ts index 07c4472..1c4fc30 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -41,6 +41,7 @@ export function extractZip(bytes: Buffer, targetDir: string): string[] { // A lemezre írás csak a teljes szűrés után történik, így ha valamelyik // korlát hibát dob, semmi nem kerül a célmappába. let total = 0; + const normalizedPaths: string[] = []; let entryCount = 0; const entries = unzipSync(new Uint8Array(bytes), { filter(info) { @@ -58,9 +59,20 @@ export function extractZip(bytes: Buffer, targetDir: string): string[] { if (total > MAX_TOTAL_BYTES) { throw new Error("A zip kibontott mérete meghaladja a 200 MB-os korlátot."); } + normalizedPaths.push(posix.normalize(info.name.replace(/\\/g, "/")).normalize("NFC").toLowerCase()); return true; } }); + const targets = new Set(normalizedPaths); + const nestedConflict = normalizedPaths.some((name) => { + for (let parent = posix.dirname(name); parent !== "."; parent = posix.dirname(parent)) { + if (targets.has(parent)) return true; + } + return false; + }); + if (targets.size !== normalizedPaths.length || targets.has(".") || nestedConflict) { + throw new Error("Ütköző útvonalak a zipben, a kibontás leáll."); + } const written: string[] = []; for (const [name, data] of Object.entries(entries)) { diff --git a/test/extract.test.ts b/test/extract.test.ts index 52d9af8..e9d8170 100644 --- a/test/extract.test.ts +++ b/test/extract.test.ts @@ -74,6 +74,18 @@ function buildManyEntryZip(entryCount: number): Buffer { } describe("extractZip", () => { + it.each([ + ["a", "a/b"], ["A", "a"], ["a/b", "a\\b"], + ["a/./b", "a/b"], ["é", "e\u0301"] + ].flatMap(([a, b]) => [[a, b], [b, a]]))("rejects conflicting %s and %s before writing", (a, b) => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tarhely-conflict-")); + try { + const zip = Buffer.from(zipSync({ "safe.txt": Buffer.from("ok"), [a]: Buffer.from("a"), [b]: Buffer.from("b") })); + expect(() => extractZip(zip, tmpDir)).toThrow("Ütköző útvonalak"); + expect(fs.readdirSync(tmpDir)).toEqual([]); + } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } + }); + it("extracts zip entries to target directory and returns relative paths", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tarhely-test-")); const zipData = Buffer.from( From 91b8c4e7b7fc3f34fc54907fa04900655d3145ca Mon Sep 17 00:00:00 2001 From: godavid Date: Fri, 13 Nov 2026 23:14:36 +0100 Subject: [PATCH 2/3] =?UTF-8?q?style(zip):=20a=20Biome=20form=C3=A1z=C3=A1?= =?UTF-8?q?s=20alkalmaz=C3=A1sa=20a=20jav=C3=ADtott=20f=C3=A1jlokra?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #7 --- src/extract.ts | 4 +++- test/extract.test.ts | 24 ++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/extract.ts b/src/extract.ts index 1c4fc30..1fd4814 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -59,7 +59,9 @@ export function extractZip(bytes: Buffer, targetDir: string): string[] { if (total > MAX_TOTAL_BYTES) { throw new Error("A zip kibontott mérete meghaladja a 200 MB-os korlátot."); } - normalizedPaths.push(posix.normalize(info.name.replace(/\\/g, "/")).normalize("NFC").toLowerCase()); + normalizedPaths.push( + posix.normalize(info.name.replace(/\\/g, "/")).normalize("NFC").toLowerCase() + ); return true; } }); diff --git a/test/extract.test.ts b/test/extract.test.ts index e9d8170..e8a890e 100644 --- a/test/extract.test.ts +++ b/test/extract.test.ts @@ -74,16 +74,28 @@ function buildManyEntryZip(entryCount: number): Buffer { } describe("extractZip", () => { - it.each([ - ["a", "a/b"], ["A", "a"], ["a/b", "a\\b"], - ["a/./b", "a/b"], ["é", "e\u0301"] - ].flatMap(([a, b]) => [[a, b], [b, a]]))("rejects conflicting %s and %s before writing", (a, b) => { + it.each( + [ + ["a", "a/b"], + ["A", "a"], + ["a/b", "a\\b"], + ["a/./b", "a/b"], + ["é", "e\u0301"] + ].flatMap(([a, b]) => [ + [a, b], + [b, a] + ]) + )("rejects conflicting %s and %s before writing", (a, b) => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tarhely-conflict-")); try { - const zip = Buffer.from(zipSync({ "safe.txt": Buffer.from("ok"), [a]: Buffer.from("a"), [b]: Buffer.from("b") })); + const zip = Buffer.from( + zipSync({ "safe.txt": Buffer.from("ok"), [a]: Buffer.from("a"), [b]: Buffer.from("b") }) + ); expect(() => extractZip(zip, tmpDir)).toThrow("Ütköző útvonalak"); expect(fs.readdirSync(tmpDir)).toEqual([]); - } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } }); it("extracts zip entries to target directory and returns relative paths", () => { From 1c3b7f60a57f6f0a1e704bdc31db7aabc01caee5 Mon Sep 17 00:00:00 2001 From: godavid Date: Fri, 13 Nov 2026 23:27:46 +0100 Subject: [PATCH 3/3] =?UTF-8?q?fix(test):=20az=20=C3=BAtvonalp=C3=A1rok=20?= =?UTF-8?q?tuple-t=C3=ADpus=C3=A1nak=20megad=C3=A1sa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #7 --- test/extract.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/extract.test.ts b/test/extract.test.ts index e8a890e..13a29ff 100644 --- a/test/extract.test.ts +++ b/test/extract.test.ts @@ -75,13 +75,15 @@ function buildManyEntryZip(entryCount: number): Buffer { describe("extractZip", () => { it.each( - [ - ["a", "a/b"], - ["A", "a"], - ["a/b", "a\\b"], - ["a/./b", "a/b"], - ["é", "e\u0301"] - ].flatMap(([a, b]) => [ + ( + [ + ["a", "a/b"], + ["A", "a"], + ["a/b", "a\\b"], + ["a/./b", "a/b"], + ["é", "e\u0301"] + ] as const + ).flatMap<[string, string]>(([a, b]) => [ [a, b], [b, a] ])