diff --git a/backend/src/routes/scannerInboxGateway.ts b/backend/src/routes/scannerInboxGateway.ts index f9c2235f..4d5dce0c 100644 --- a/backend/src/routes/scannerInboxGateway.ts +++ b/backend/src/routes/scannerInboxGateway.ts @@ -26,7 +26,9 @@ export default async function scannerInboxGateway(server: FastifyInstance, optio const handle = async (req: FastifyRequest, reply: any) => { const identity = identities.get(req)! - const filename = (req.params as { "*"?: string })["*"] || "" + // Epson appends its own separator even when the configured inbox URL ends in '/'. + // Remove only extra separators at the collection boundary, not within filenames. + const filename = ((req.params as { "*"?: string })["*"] || "").replace(/^\/+/, "") if (filename && (filename.length > 200 || filename.startsWith(".") || /[\\/\u0000-\u001f\u007f]/.test(filename))) { throw scannerError(400, "Unterordner und ungültige Dateinamen werden nicht unterstützt.") } diff --git a/backend/tests/scannerInbox.test.ts b/backend/tests/scannerInbox.test.ts index 96fda3c0..0a7eb483 100644 --- a/backend/tests/scannerInbox.test.ts +++ b/backend/tests/scannerInbox.test.ts @@ -101,6 +101,23 @@ test("Epson discovery works with both slash forms, XML bodies and a backend prox assert.equal((await app.inject({ method: "PROPFIND", url: "/scanner-inbox/", headers: { ...basic(), depth: "infinity" } })).statusCode, 403) }) +test("Epson's extra separator does not turn filename checks into false collisions", async t => { + const { app, calls } = await protocolFixture() + t.after(() => app.close()) + for (const separator of ["//", "///"]) { + const url = `/scanner-inbox${separator}scan.pdf` + assert.equal((await app.inject({ method: "HEAD", url, headers: basic() })).statusCode, 404) + const upload = await app.inject({ method: "PUT", url, headers: basic(), payload: samplePdf }) + assert.equal(upload.statusCode, 201) + assert.equal(calls.at(-1)[1], "scan.pdf") + assert.deepEqual(calls.at(-1)[2], samplePdf) + for (const name of ["sub/scan.pdf", "sub%2Fscan.pdf", "..%5Cscan.pdf", ".hidden.pdf", "%2e%2e%2Fscan.pdf"]) { + assert.equal((await app.inject({ method: "PUT", url: `/scanner-inbox${separator}${name}`, headers: basic(), payload: samplePdf })).statusCode, 400) + } + } + assert.equal(calls.length, 2) +}) + test("raw PDFs reach storage unchanged and scanner credentials cannot download or delete", async t => { const { app, calls } = await protocolFixture() t.after(() => app.close()) @@ -211,6 +228,10 @@ test("scanner inbox integration: migration, persistence, isolation, retries and assert.equal(listing.json().total, 1) assert.equal(listing.json().documents[0].name, "scan.pdf") assert.equal((await app.inject({ method: "HEAD", url: "/scanner-inbox/scan.pdf", headers: basic(first.username, first.password) })).statusCode, 200) + assert.equal((await app.inject({ method: "HEAD", url: "/scanner-inbox//scan.pdf", headers: basic(first.username, first.password) })).statusCode, 200) + assert.equal((await app.inject({ method: "PUT", url: "/scanner-inbox//scan.pdf", headers: basic(first.username, first.password), payload: pdf })).statusCode, 204) + assert.equal((await service.list(1, 50, 0)).total, 1) + assert.equal((await app.inject({ method: "GET", url: "/scanner-inbox//scan.pdf", headers: basic(first.username, first.password) })).statusCode, 405) const download = await app.inject({ method: "GET", url: "/scanner-inbox/scan.pdf", headers: basic(first.username, first.password) }) assert.equal(download.statusCode, 405) assert.ok(!(download.headers.allow as string).split(/,\s*/).includes("GET")) diff --git a/docs/scanner-dokumentenablage.md b/docs/scanner-dokumentenablage.md index c1afab9b..408d5e8a 100644 --- a/docs/scanner-dokumentenablage.md +++ b/docs/scanner-dokumentenablage.md @@ -22,7 +22,7 @@ Beim ES-580W nach einem Neustart eine gegebenenfalls angezeigte Freigabe der Net Die vollständige Adresse wird aus der konfigurierten Backend-Adresse gebildet, z. B. `https://fedeo.example/backend/scanner-inbox/`. Ein vorhandener `/backend`-Präfix muss erhalten bleiben. Es ist kein lokaler Rechner oder Geräte-Agent erforderlich. -Grundlage sind die Epson-Handbücher für [ES-580W](https://download.epson.de/pub/download/6371/epson637123eu.pdf) und [ES-C380W](https://download4.epson.biz/sec_pubs/es-c380w/useg/en/GUID-55C5993A-63B2-45C9-B3C9-9C6D6801CDF0.htm). Am physischen ES-580W wurde eine anfängliche `GET`-Prüfung des Empfangsordners beobachtet: Auf die `401`-Anmeldeaufforderung folgt ein authentifiziertes `GET`. Dieses muss erfolgreich beantwortet werden. Einen vollständigen Scan bis zum Eintrag in der Dokumentenablage bei der Inbetriebnahme prüfen. +Grundlage sind die Epson-Handbücher für [ES-580W](https://download.epson.de/pub/download/6371/epson637123eu.pdf) und [ES-C380W](https://download4.epson.biz/sec_pubs/es-c380w/useg/en/GUID-55C5993A-63B2-45C9-B3C9-9C6D6801CDF0.htm). Am physischen ES-580W wurde eine anfängliche `GET`-Prüfung des Empfangsordners beobachtet: Auf die `401`-Anmeldeaufforderung folgt ein authentifiziertes `GET`. Dieses muss erfolgreich beantwortet werden. Danach prüft Epson den Dateinamen per `HEAD`. Bei einer Zieladresse mit abschließendem Schrägstrich hängt das Gerät einen weiteren Trenner an, beispielsweise `/scanner-inbox//scan.pdf`. Der Endpunkt behandelt diese Adresse wie `/scanner-inbox/scan.pdf`; echte Unterordner und ungültige Dateinamen bleiben gesperrt. Ein fälschlich mit `400` beantworteter HEAD-Test führte am Gerät zur Meldung „Dateiname bereits verwendet“. Einen vollständigen Scan bis zum Eintrag in der Dokumentenablage bei der Inbetriebnahme prüfen. ## Schnittstelle und Betrieb