Skip to content

Commit e6686c1

Browse files
authored
Merge pull request #7601 from LibreSign/backport/7598/stable33
[stable33] fix(validation): harden signed file validation handling
2 parents 3bc58f1 + cffb1f8 commit e6686c1

5 files changed

Lines changed: 87 additions & 6 deletions

File tree

lib/Service/File/CertificateChainService.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public function getCertificateChain($fileNode, File $libreSignFile, $options): a
2727

2828
try {
2929
$resource = $fileNode->fopen('rb');
30+
if (!is_resource($resource)) {
31+
$this->logger->warning('Failed to load certificate chain: unable to open signed file stream');
32+
return [];
33+
}
3034
$sha256 = $this->getSha256FromResource($resource);
3135
rewind($resource);
3236
if ($sha256 === $libreSignFile->getSignedHash()) {
@@ -42,9 +46,16 @@ public function getCertificateChain($fileNode, File $libreSignFile, $options): a
4246
}
4347

4448
private function getSha256FromResource($resource): string {
49+
if (!is_resource($resource)) {
50+
return '';
51+
}
52+
4553
$hashContext = hash_init('sha256');
4654
while (!feof($resource)) {
4755
$buffer = fread($resource, 8192);
56+
if ($buffer === false) {
57+
break;
58+
}
4859
hash_update($hashContext, $buffer);
4960
}
5061
return hash_final($hashContext);

lib/Service/File/EnvelopeAssembler.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ public function buildEnvelopeChildData(File $childFile, \OCA\Libresign\Service\F
143143
$certData = $this->certificateChainService->getCertificateChain($fileNode, $childFile, $options);
144144
} else {
145145
$resource = $fileNode->fopen('rb');
146+
if (!is_resource($resource)) {
147+
throw new \RuntimeException('unable to open signed file stream');
148+
}
146149
$sha256 = $this->getSha256FromResource($resource);
147150
rewind($resource);
148151
if ($sha256 === $childFile->getSignedHash()) {
@@ -164,9 +167,16 @@ public function buildEnvelopeChildData(File $childFile, \OCA\Libresign\Service\F
164167
}
165168

166169
private function getSha256FromResource($resource): string {
170+
if (!is_resource($resource)) {
171+
return '';
172+
}
173+
167174
$hashContext = hash_init('sha256');
168175
while (!feof($resource)) {
169176
$buffer = fread($resource, 8192);
177+
if ($buffer === false) {
178+
break;
179+
}
170180
hash_update($hashContext, $buffer);
171181
}
172182
return hash_final($hashContext);

src/services/validationDocument.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@ function isOptionalField(record: UnknownRecord, key: string, guard: (value: unkn
6464
}
6565

6666
function toNumber(value: unknown): number | null {
67-
return typeof value === 'number' && Number.isFinite(value) ? value : null
67+
if (typeof value === 'number' && Number.isFinite(value)) {
68+
return value
69+
}
70+
71+
if (typeof value === 'string' && /^-?\d+$/.test(value)) {
72+
return Number.parseInt(value, 10)
73+
}
74+
75+
return null
6876
}
6977

7078
function isString(value: unknown): value is string {
@@ -91,6 +99,15 @@ function isSignerStatus(value: unknown): value is SignerDetailRecord['status'] {
9199
|| normalizedValue === SIGN_REQUEST_STATUS.SIGNED
92100
}
93101

102+
function isValidationSignatureFlow(value: unknown): boolean {
103+
if (value === 'none' || value === 'parallel' || value === 'ordered_numeric') {
104+
return true
105+
}
106+
107+
const normalizedValue = toNumber(value)
108+
return normalizedValue === 0 || normalizedValue === 1 || normalizedValue === 2
109+
}
110+
94111
function isValidationStatusInfo(value: unknown): value is ValidationStatusInfo {
95112
if (!isRecord(value)) {
96113
return false
@@ -210,12 +227,12 @@ function isValidationDocumentRecord(data: unknown): data is ValidationFileRecord
210227
|| !isString(data.statusText)
211228
|| typeof data.nodeId !== 'number'
212229
|| (data.nodeType !== 'file' && data.nodeType !== 'envelope')
213-
|| typeof data.signatureFlow !== 'number'
214-
|| typeof data.docmdpLevel !== 'number'
215-
|| typeof data.filesCount !== 'number'
230+
|| !isValidationSignatureFlow(data.signatureFlow)
231+
|| toNumber(data.docmdpLevel) === null
232+
|| toNumber(data.filesCount) === null
216233
|| !Array.isArray(data.files)
217-
|| typeof data.totalPages !== 'number'
218-
|| typeof data.size !== 'number'
234+
|| toNumber(data.totalPages) === null
235+
|| toNumber(data.size) === null
219236
|| !isString(data.pdfVersion)
220237
|| !isString(data.created_at)
221238
|| !isRequestedBy(data.requested_by)

src/tests/services/validationDocument.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ describe('validationDocument', () => {
106106
}))
107107
})
108108

109+
it('accepts validation payload when signatureFlow is enum string and numeric fields are numeric strings', () => {
110+
const normalized = toValidationDocument(createValidationPayload({
111+
signatureFlow: 'none',
112+
docmdpLevel: '2',
113+
filesCount: '1',
114+
totalPages: '1',
115+
size: '10',
116+
signers: [createSigner({ status: '2' })],
117+
}))
118+
119+
expect(normalized).not.toBeNull()
120+
expect(normalized?.signatureFlow).toBe('none')
121+
})
122+
109123
it('rejects payload with invalid signer status', () => {
110124
const normalized = toValidationDocument(createValidationPayload({
111125
signers: [createSigner({ status: 99, statusText: 'Invalid' })],

tests/php/Unit/Service/File/CertificateChainServiceTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,33 @@ public function fopen($mode) {
5353
$this->assertIsArray($result);
5454
$this->assertArrayHasKey('chain', $result);
5555
}
56+
57+
public function testGetCertificateChainHandlesInvalidResourceGracefully(): void {
58+
$fileNode = new class() {
59+
public function fopen($mode) {
60+
return false;
61+
}
62+
};
63+
64+
$libreSignFile = new File();
65+
$libreSignFile->setSignedNodeId(1);
66+
67+
$pkcs12 = $this->createMock(Pkcs12Handler::class);
68+
$pkcs12->expects($this->never())->method('getCertificateChain');
69+
70+
$logger = $this->createMock(LoggerInterface::class);
71+
$logger
72+
->expects($this->once())
73+
->method('warning')
74+
->with($this->stringContains('unable to open signed file stream'));
75+
76+
$service = new CertificateChainService($pkcs12, $logger);
77+
78+
$options = new FileResponseOptions();
79+
$options->validateFile(true);
80+
81+
$result = $service->getCertificateChain($fileNode, $libreSignFile, $options);
82+
83+
$this->assertSame([], $result);
84+
}
5685
}

0 commit comments

Comments
 (0)