From 81b6595d303bee521afffd29753df201e53aa5b4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:52:17 -0300 Subject: [PATCH 1/5] fix(validation): guard invalid signed file streams in CertificateChainService Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/File/CertificateChainService.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/Service/File/CertificateChainService.php b/lib/Service/File/CertificateChainService.php index 1fa248c4a9..d13f71d7fa 100644 --- a/lib/Service/File/CertificateChainService.php +++ b/lib/Service/File/CertificateChainService.php @@ -27,6 +27,10 @@ public function getCertificateChain($fileNode, File $libreSignFile, $options): a try { $resource = $fileNode->fopen('rb'); + if (!is_resource($resource)) { + $this->logger->warning('Failed to load certificate chain: unable to open signed file stream'); + return []; + } $sha256 = $this->getSha256FromResource($resource); rewind($resource); if ($sha256 === $libreSignFile->getSignedHash()) { @@ -42,9 +46,16 @@ public function getCertificateChain($fileNode, File $libreSignFile, $options): a } private function getSha256FromResource($resource): string { + if (!is_resource($resource)) { + return ''; + } + $hashContext = hash_init('sha256'); while (!feof($resource)) { $buffer = fread($resource, 8192); + if ($buffer === false) { + break; + } hash_update($hashContext, $buffer); } return hash_final($hashContext); From ed2c5271280e1afc4fd997ca56a8c611cb5025fe Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:52:17 -0300 Subject: [PATCH 2/5] fix(validation): guard invalid envelope signed file streams Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/File/EnvelopeAssembler.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/Service/File/EnvelopeAssembler.php b/lib/Service/File/EnvelopeAssembler.php index e5025492f6..cb6c304da3 100644 --- a/lib/Service/File/EnvelopeAssembler.php +++ b/lib/Service/File/EnvelopeAssembler.php @@ -143,6 +143,9 @@ public function buildEnvelopeChildData(File $childFile, \OCA\Libresign\Service\F $certData = $this->certificateChainService->getCertificateChain($fileNode, $childFile, $options); } else { $resource = $fileNode->fopen('rb'); + if (!is_resource($resource)) { + throw new \RuntimeException('unable to open signed file stream'); + } $sha256 = $this->getSha256FromResource($resource); rewind($resource); if ($sha256 === $childFile->getSignedHash()) { @@ -164,9 +167,16 @@ public function buildEnvelopeChildData(File $childFile, \OCA\Libresign\Service\F } private function getSha256FromResource($resource): string { + if (!is_resource($resource)) { + return ''; + } + $hashContext = hash_init('sha256'); while (!feof($resource)) { $buffer = fread($resource, 8192); + if ($buffer === false) { + break; + } hash_update($hashContext, $buffer); } return hash_final($hashContext); From 93bbed3d363f1b5d2db408169f2193f1a137825d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:52:17 -0300 Subject: [PATCH 3/5] fix(validation): tolerate runtime validation payload variations Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/services/validationDocument.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/services/validationDocument.ts b/src/services/validationDocument.ts index f6545f233a..4cfcb816cb 100644 --- a/src/services/validationDocument.ts +++ b/src/services/validationDocument.ts @@ -64,7 +64,15 @@ function isOptionalField(record: UnknownRecord, key: string, guard: (value: unkn } function toNumber(value: unknown): number | null { - return typeof value === 'number' && Number.isFinite(value) ? value : null + if (typeof value === 'number' && Number.isFinite(value)) { + return value + } + + if (typeof value === 'string' && /^-?\d+$/.test(value)) { + return Number.parseInt(value, 10) + } + + return null } function isString(value: unknown): value is string { @@ -91,6 +99,15 @@ function isSignerStatus(value: unknown): value is SignerDetailRecord['status'] { || normalizedValue === SIGN_REQUEST_STATUS.SIGNED } +function isValidationSignatureFlow(value: unknown): boolean { + if (value === 'none' || value === 'parallel' || value === 'ordered_numeric') { + return true + } + + const normalizedValue = toNumber(value) + return normalizedValue === 0 || normalizedValue === 1 || normalizedValue === 2 +} + function isValidationStatusInfo(value: unknown): value is ValidationStatusInfo { if (!isRecord(value)) { return false @@ -210,12 +227,12 @@ function isValidationDocumentRecord(data: unknown): data is ValidationFileRecord || !isString(data.statusText) || typeof data.nodeId !== 'number' || (data.nodeType !== 'file' && data.nodeType !== 'envelope') - || typeof data.signatureFlow !== 'number' - || typeof data.docmdpLevel !== 'number' - || typeof data.filesCount !== 'number' + || !isValidationSignatureFlow(data.signatureFlow) + || toNumber(data.docmdpLevel) === null + || toNumber(data.filesCount) === null || !Array.isArray(data.files) - || typeof data.totalPages !== 'number' - || typeof data.size !== 'number' + || toNumber(data.totalPages) === null + || toNumber(data.size) === null || !isString(data.pdfVersion) || !isString(data.created_at) || !isRequestedBy(data.requested_by) From d07d7bbac2db3cc7f9db75b77caee97f0937f83e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:52:17 -0300 Subject: [PATCH 4/5] test(validation): cover string-based runtime payload normalization Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/services/validationDocument.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/tests/services/validationDocument.spec.ts b/src/tests/services/validationDocument.spec.ts index c223eefcdf..832a01b877 100644 --- a/src/tests/services/validationDocument.spec.ts +++ b/src/tests/services/validationDocument.spec.ts @@ -106,6 +106,20 @@ describe('validationDocument', () => { })) }) + it('accepts validation payload when signatureFlow is enum string and numeric fields are numeric strings', () => { + const normalized = toValidationDocument(createValidationPayload({ + signatureFlow: 'none', + docmdpLevel: '2', + filesCount: '1', + totalPages: '1', + size: '10', + signers: [createSigner({ status: '2' })], + })) + + expect(normalized).not.toBeNull() + expect(normalized?.signatureFlow).toBe('none') + }) + it('rejects payload with invalid signer status', () => { const normalized = toValidationDocument(createValidationPayload({ signers: [createSigner({ status: 99, statusText: 'Invalid' })], From 6ac16f80ec5b022d4217063781aa4d05845a9225 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:52:17 -0300 Subject: [PATCH 5/5] test(validation): cover invalid certificate chain stream handling Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../File/CertificateChainServiceTest.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/php/Unit/Service/File/CertificateChainServiceTest.php b/tests/php/Unit/Service/File/CertificateChainServiceTest.php index 226bb6f73f..4e6d0669db 100644 --- a/tests/php/Unit/Service/File/CertificateChainServiceTest.php +++ b/tests/php/Unit/Service/File/CertificateChainServiceTest.php @@ -53,4 +53,33 @@ public function fopen($mode) { $this->assertIsArray($result); $this->assertArrayHasKey('chain', $result); } + + public function testGetCertificateChainHandlesInvalidResourceGracefully(): void { + $fileNode = new class() { + public function fopen($mode) { + return false; + } + }; + + $libreSignFile = new File(); + $libreSignFile->setSignedNodeId(1); + + $pkcs12 = $this->createMock(Pkcs12Handler::class); + $pkcs12->expects($this->never())->method('getCertificateChain'); + + $logger = $this->createMock(LoggerInterface::class); + $logger + ->expects($this->once()) + ->method('warning') + ->with($this->stringContains('unable to open signed file stream')); + + $service = new CertificateChainService($pkcs12, $logger); + + $options = new FileResponseOptions(); + $options->validateFile(true); + + $result = $service->getCertificateChain($fileNode, $libreSignFile, $options); + + $this->assertSame([], $result); + } }