Skip to content

Commit 902cb3d

Browse files
committed
fix: validate written size for s3 multipart uploads
Signed-off-by: Robin Appelman <robin@icewind.nl>
1 parent e483387 commit 902cb3d

File tree

3 files changed

+36
-9
lines changed

3 files changed

+36
-9
lines changed

apps/dav/lib/Connector/Sabre/File.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ public function put($data) {
201201
}
202202
}
203203

204+
$lengthHeader = $this->request->getHeader('content-length');
205+
$expected = $lengthHeader !== '' ? (int)$lengthHeader : null;
206+
204207
if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
205208
$isEOF = false;
206209
$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
@@ -212,7 +215,7 @@ public function put($data) {
212215
$count = -1;
213216
try {
214217
/** @var IWriteStreamStorage $partStorage */
215-
$count = $partStorage->writeStream($internalPartPath, $wrappedData);
218+
$count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected);
216219
} catch (GenericFileException $e) {
217220
$logger = Server::get(LoggerInterface::class);
218221
$logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
@@ -232,10 +235,7 @@ public function put($data) {
232235
[$count, $result] = \OC_Helper::streamCopy($data, $target);
233236
fclose($target);
234237
}
235-
236-
$lengthHeader = $this->request->getHeader('content-length');
237-
$expected = $lengthHeader !== '' ? (int)$lengthHeader : -1;
238-
if ($result === false && $expected >= 0) {
238+
if ($result === false && $expected !== null) {
239239
throw new Exception(
240240
$this->l10n->t(
241241
'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
@@ -250,7 +250,7 @@ public function put($data) {
250250
// if content length is sent by client:
251251
// double check if the file was fully received
252252
// compare expected and actual size
253-
if ($expected >= 0
253+
if ($expected !== null
254254
&& $expected !== $count
255255
&& $this->request->getMethod() === 'PUT'
256256
) {

lib/private/Files/ObjectStore/ObjectStoreStorage.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,9 @@ public function writeStream(string $path, $stream, ?int $size = null): int {
482482
$metadata = [
483483
'mimetype' => $mimetype,
484484
];
485+
if ($size) {
486+
$metadata['size'] = $size;
487+
}
485488

486489
$stat['mimetype'] = $mimetype;
487490
$stat['etag'] = $this->getETag($path);

lib/private/Files/ObjectStore/S3ObjectTrait.php

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
*/
77
namespace OC\Files\ObjectStore;
88

9+
use Aws\Command;
10+
use Aws\Exception\MultipartUploadException;
911
use Aws\S3\Exception\S3MultipartUploadException;
1012
use Aws\S3\MultipartCopy;
1113
use Aws\S3\MultipartUploader;
@@ -87,14 +89,20 @@ public function readObject($urn) {
8789
* @throws \Exception when something goes wrong, message will be logged
8890
*/
8991
protected function writeSingle(string $urn, StreamInterface $stream, array $metaData): void {
90-
$this->getConnection()->putObject([
92+
$args = [
9193
'Bucket' => $this->bucket,
9294
'Key' => $urn,
9395
'Body' => $stream,
9496
'ACL' => 'private',
9597
'ContentType' => $metaData['mimetype'] ?? null,
9698
'StorageClass' => $this->storageClass,
97-
] + $this->getSSECParameters());
99+
] + $this->getSSECParameters();
100+
101+
if ($size = $stream->getSize()) {
102+
$args['ContentLength'] = $size;
103+
}
104+
105+
$this->getConnection()->putObject($args);
98106
}
99107

100108

@@ -112,6 +120,8 @@ protected function writeMultiPart(string $urn, StreamInterface $stream, array $m
112120
$concurrency = $this->concurrency;
113121
$exception = null;
114122
$state = null;
123+
$size = $stream->getSize();
124+
$totalWritten = 0;
115125

116126
// retry multipart upload once with concurrency at half on failure
117127
while (!$uploaded && $attempts <= 1) {
@@ -125,6 +135,15 @@ protected function writeMultiPart(string $urn, StreamInterface $stream, array $m
125135
'ContentType' => $metaData['mimetype'] ?? null,
126136
'StorageClass' => $this->storageClass,
127137
] + $this->getSSECParameters(),
138+
'before_upload' => function (Command $command) use (&$totalWritten) {
139+
$totalWritten += $command['ContentLength'];
140+
},
141+
'before_complete' => function ($_command) use (&$totalWritten, $size, &$uploader, &$attempts) {
142+
if ($size !== null && $totalWritten != $size) {
143+
$e = new \Exception('Incomplete multi part upload, expected ' . $size . ' bytes, wrote ' . $totalWritten);
144+
throw new MultipartUploadException($uploader->getState(), $e);
145+
}
146+
},
128147
]);
129148

130149
try {
@@ -141,6 +160,9 @@ protected function writeMultiPart(string $urn, StreamInterface $stream, array $m
141160
if ($stream->isSeekable()) {
142161
$stream->rewind();
143162
}
163+
} catch (MultipartUploadException $e) {
164+
$exception = $e;
165+
break;
144166
}
145167
}
146168

@@ -166,7 +188,9 @@ public function writeObject($urn, $stream, ?string $mimetype = null) {
166188

167189
public function writeObjectWithMetaData(string $urn, $stream, array $metaData): void {
168190
$canSeek = fseek($stream, 0, SEEK_CUR) === 0;
169-
$psrStream = Utils::streamFor($stream);
191+
$psrStream = Utils::streamFor($stream, [
192+
'size' => $metaData['size'] ?? null,
193+
]);
170194

171195

172196
$size = $psrStream->getSize();

0 commit comments

Comments
 (0)