• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tomdesair / tus-java-server / 27478147406

13 Jun 2026 08:26PM UTC coverage: 94.92% (+0.01%) from 94.91%
27478147406

Pull #85

github

web-flow
Merge 56a3ed9a2 into ec406348a
Pull Request #85: Validate and canonicalize Upload-Checksum value to prevent path traversal vulnerability

621 of 698 branches covered (88.97%)

Branch coverage included in aggregate %.

35 of 35 new or added lines in 16 files covered. (100.0%)

18 existing lines in 4 files now uncovered.

1808 of 1861 relevant lines covered (97.15%)

6.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.63
/src/main/java/me/desair/tus/server/upload/disk/DiskStorageService.java
1
package me.desair.tus.server.upload.disk;
2

3
import static java.nio.file.StandardOpenOption.READ;
4
import static java.nio.file.StandardOpenOption.WRITE;
5

6
import java.io.File;
7
import java.io.IOException;
8
import java.io.InputStream;
9
import java.io.OutputStream;
10
import java.nio.channels.Channels;
11
import java.nio.channels.FileChannel;
12
import java.nio.channels.ReadableByteChannel;
13
import java.nio.channels.WritableByteChannel;
14
import java.nio.charset.StandardCharsets;
15
import java.nio.file.DirectoryStream;
16
import java.nio.file.Files;
17
import java.nio.file.Path;
18
import java.util.Collections;
19
import java.util.List;
20
import java.util.Objects;
21
import me.desair.tus.server.checksum.ChecksumAlgorithm;
22
import me.desair.tus.server.exception.InvalidUploadOffsetException;
23
import me.desair.tus.server.exception.TusException;
24
import me.desair.tus.server.exception.UploadNotFoundException;
25
import me.desair.tus.server.upload.UploadId;
26
import me.desair.tus.server.upload.UploadIdFactory;
27
import me.desair.tus.server.upload.UploadInfo;
28
import me.desair.tus.server.upload.UploadLockingService;
29
import me.desair.tus.server.upload.UploadStorageService;
30
import me.desair.tus.server.upload.UploadType;
31
import me.desair.tus.server.upload.concatenation.UploadConcatenationService;
32
import me.desair.tus.server.upload.concatenation.VirtualConcatenationService;
33
import me.desair.tus.server.util.Utils;
34
import org.apache.commons.io.FileUtils;
35
import org.slf4j.Logger;
36
import org.slf4j.LoggerFactory;
37

38
/** Implementation of {@link UploadStorageService} that implements storage on disk. */
39
public class DiskStorageService extends AbstractDiskBasedService implements UploadStorageService {
40

41
  private static final Logger log = LoggerFactory.getLogger(DiskStorageService.class);
8✔
42

43
  private static final String UPLOAD_SUB_DIRECTORY = "uploads";
44
  private static final String INFO_FILE = "info";
45
  private static final String DATA_FILE = "data";
46

47
  private Long maxUploadSize = null;
6✔
48
  private Long uploadExpirationPeriod = null;
6✔
49
  private UploadIdFactory idFactory;
50
  private UploadConcatenationService uploadConcatenationService;
51
  private boolean isUploadDeduplicationEnabled = false;
6✔
52

53
  public DiskStorageService(String storagePath) {
54
    super(storagePath + File.separator + UPLOAD_SUB_DIRECTORY);
10✔
55
    setUploadConcatenationService(new VirtualConcatenationService(this));
12✔
56
  }
2✔
57

58
  public DiskStorageService(UploadIdFactory idFactory, String storagePath) {
59
    this(storagePath);
6✔
60
    Objects.requireNonNull(idFactory, "The IdFactory cannot be null");
8✔
61
    this.idFactory = idFactory;
6✔
62
  }
2✔
63

64
  @Override
65
  public void setIdFactory(UploadIdFactory idFactory) {
66
    Objects.requireNonNull(idFactory, "The IdFactory cannot be null");
4✔
67
    this.idFactory = idFactory;
3✔
68
  }
1✔
69

70
  @Override
71
  public void setMaxUploadSize(Long maxUploadSize) {
72
    this.maxUploadSize = (maxUploadSize != null && maxUploadSize > 0 ? maxUploadSize : 0);
28✔
73
  }
2✔
74

75
  @Override
76
  public long getMaxUploadSize() {
77
    return maxUploadSize == null ? 0 : maxUploadSize;
18✔
78
  }
79

80
  @Override
81
  public void setUploadDeduplicationEnabled(boolean enabled) {
82
    this.isUploadDeduplicationEnabled = enabled;
6✔
83
  }
2✔
84

85
  @Override
86
  public boolean isUploadDeduplicationEnabled() {
87
    return this.isUploadDeduplicationEnabled;
6✔
88
  }
89

90
  @Override
91
  public UploadInfo getUploadInfoByChecksum(String checksum, ChecksumAlgorithm algorithm)
92
      throws IOException {
93
    if (checksum == null || algorithm == null) {
8✔
94
      return null;
2✔
95
    }
96
    Path checksumFile = getChecksumPath(checksum, algorithm);
10✔
97
    if (Files.exists(checksumFile)) {
10✔
98
      String uploadIdStr =
6✔
99
          new String(Files.readAllBytes(checksumFile), StandardCharsets.UTF_8).trim();
10✔
100
      UploadId id = new UploadId(uploadIdStr);
10✔
101
      UploadInfo info = getUploadInfo(id);
8✔
102
      if (info == null) {
4✔
103
        Files.deleteIfExists(checksumFile);
3✔
104
        return null;
2✔
105
      }
106
      Path bytesPath = null;
4✔
107
      try {
108
        bytesPath = getPathInUploadDir(id, DATA_FILE);
10✔
UNCOV
109
      } catch (UploadNotFoundException e) {
×
110
        // file doesn't exist
111
      }
2✔
112
      if (bytesPath == null || !Files.exists(bytesPath)) {
14!
113
        Files.deleteIfExists(checksumFile);
3✔
114
        return null;
2✔
115
      }
116
      return info;
4✔
117
    }
118
    return null;
4✔
119
  }
120

121
  @Override
122
  public UploadInfo getUploadInfo(String uploadUrl, String ownerKey) throws IOException {
123
    UploadInfo uploadInfo = getUploadInfo(idFactory.readUploadId(uploadUrl));
14✔
124
    if (uploadInfo == null || !Objects.equals(uploadInfo.getOwnerKey(), ownerKey)) {
14✔
125
      return null;
4✔
126
    } else {
127
      return uploadInfo;
4✔
128
    }
129
  }
130

131
  @Override
132
  public UploadInfo getUploadInfo(UploadId id) throws IOException {
133
    try {
134
      Path infoPath = getInfoPath(id);
8✔
135
      if (infoPath == null || !Files.exists(infoPath)) {
14!
136
        return null;
2✔
137
      }
138
      return Utils.readSerializable(infoPath, UploadInfo.class);
10✔
139
    } catch (UploadNotFoundException e) {
2✔
140
      return null;
4✔
141
    }
142
  }
143

144
  @Override
145
  public String getUploadUri() {
146
    return idFactory.getUploadUri();
8✔
147
  }
148

149
  @Override
150
  public UploadInfo create(UploadInfo info, String ownerKey) throws IOException {
151
    UploadId id = createNewId();
6✔
152

153
    createUploadDirectory(id);
8✔
154

155
    try {
156
      Path bytesPath = getBytesPath(id);
8✔
157

158
      // Create an empty file to storage the bytes of this upload
159
      Files.createFile(bytesPath);
10✔
160

161
      // Set starting values
162
      info.setId(id);
6✔
163
      info.setOffset(0L);
8✔
164
      info.setOwnerKey(ownerKey);
6✔
165

166
      update(info);
6✔
167

168
      return info;
4✔
UNCOV
169
    } catch (UploadNotFoundException e) {
×
170
      // Normally this cannot happen
171
      log.error("Unable to create UploadInfo because of an upload not found exception", e);
×
UNCOV
172
      return null;
×
173
    }
174
  }
175

176
  @Override
177
  public void update(UploadInfo uploadInfo) throws IOException, UploadNotFoundException {
178
    if (uploadInfo != null) {
4!
179
      if (uploadInfo.getDuplicatesUploadId() != null) {
6✔
180
        // Delete the child's own data file if it exists
181
        try {
182
          Path childDataPath = getPathInUploadDir(uploadInfo.getId(), DATA_FILE);
12✔
183
          Files.deleteIfExists(childDataPath);
6✔
UNCOV
184
        } catch (UploadNotFoundException e) {
×
185
          // It doesn't exist yet, which is fine
186
        }
2✔
187

188
        // Retrieve the parent UploadInfo
189
        UploadId parentId = uploadInfo.getDuplicatesUploadId();
6✔
190
        UploadInfo parentInfo = getUploadInfo(parentId);
8✔
191
        if (parentInfo != null) {
4!
192
          Path parentBytesPath = null;
4✔
193
          try {
194
            parentBytesPath = getPathInUploadDir(parentId, DATA_FILE);
10✔
UNCOV
195
          } catch (UploadNotFoundException e) {
×
196
            // Parent file is not found
197
          }
2✔
198

199
          if (parentBytesPath != null && Files.exists(parentBytesPath)) {
14!
200
            // Ensure parent's expiration timestamp is >= child's expiration timestamp
201
            Long childExpire = uploadInfo.getExpirationTimestamp();
6✔
202
            Long parentExpire = parentInfo.getExpirationTimestamp();
6✔
203
            if (childExpire == null) {
4✔
204
              if (parentExpire != null) {
2✔
205
                parentInfo.setExpirationTimestamp(null);
3✔
206
                Path parentInfoPath = getInfoPath(parentId);
4✔
207
                Utils.writeSerializable(parentInfo, parentInfoPath);
3✔
208
              }
1✔
209
            } else {
210
              if (parentExpire != null && parentExpire < childExpire) {
16!
211
                parentInfo.setExpirationTimestamp(childExpire);
6✔
212
                Path parentInfoPath = getInfoPath(parentId);
8✔
213
                Utils.writeSerializable(parentInfo, parentInfoPath);
6✔
214
              }
215
            }
216
          }
217
        }
218
      } else if (isUploadDeduplicationEnabled()
10✔
219
          && !uploadInfo.isUploadInProgress()
6✔
220
          && uploadInfo.getChecksum() != null
6✔
221
          && uploadInfo.getChecksumAlgorithm() != null) {
4!
222
        // Index the checksum
223
        Path checksumFile =
4✔
224
            getChecksumPath(uploadInfo.getChecksum(), uploadInfo.getChecksumAlgorithm());
10✔
225
        Files.createDirectories(checksumFile.getParent());
12✔
226
        Files.write(checksumFile, uploadInfo.getId().toString().getBytes(StandardCharsets.UTF_8));
20✔
227
      }
228

229
      Path infoPath = getInfoPath(uploadInfo.getId());
10✔
230
      Utils.writeSerializable(uploadInfo, infoPath);
6✔
231
    }
232
  }
2✔
233

234
  @Override
235
  public UploadInfo append(UploadInfo info, InputStream inputStream)
236
      throws IOException, TusException {
237
    if (info != null) {
4!
238
      Path bytesPath = getBytesPath(info.getId());
10✔
239

240
      long max = getMaxUploadSize() > 0 ? getMaxUploadSize() : Long.MAX_VALUE;
20✔
241
      long transferred = 0;
4✔
242
      Long offset = info.getOffset();
6✔
243
      long newOffset = offset;
6✔
244

245
      try (ReadableByteChannel uploadedBytes = Channels.newChannel(inputStream);
6✔
246
          FileChannel file = FileChannel.open(bytesPath, WRITE)) {
18✔
247

248
        try {
249
          // Lock will be released when the channel closes
250
          file.lock();
6✔
251

252
          // Validate that the given offset is at the end of the file
253
          if (!offset.equals(file.size())) {
12✔
254
            throw new InvalidUploadOffsetException(
5✔
255
                "The upload offset does not correspond to the written"
256
                    + " bytes. You can only append to the end of an upload");
257
          }
258

259
          // write all bytes in the channel up to the configured maximum
260
          transferred = file.transferFrom(uploadedBytes, offset, max - offset);
20✔
261
          file.force(true);
6✔
262
          newOffset = offset + transferred;
10✔
263

264
        } catch (Exception ex) {
2✔
265
          // An error occurred, try to write as much data as possible
266
          newOffset = writeAsMuchAsPossible(file);
8✔
267
          throw ex;
4✔
268
        }
2✔
269

270
      } finally {
271
        info.setOffset(newOffset);
8✔
272
        update(info);
6✔
273
      }
274
    }
275

276
    return info;
4✔
277
  }
278

279
  @Override
280
  public void removeLastNumberOfBytes(UploadInfo info, long byteCount)
281
      throws UploadNotFoundException, IOException {
282

283
    if (info != null && byteCount > 0) {
12✔
284
      Path bytesPath = getBytesPath(info.getId());
10✔
285

286
      try (FileChannel file = FileChannel.open(bytesPath, WRITE)) {
18✔
287

288
        // Lock will be released when the channel closes
289
        file.lock();
6✔
290

291
        file.truncate(file.size() - byteCount);
14✔
292
        file.force(true);
6✔
293

294
        info.setOffset(file.size());
10✔
295
        update(info);
6✔
296
      }
297
    }
298
  }
2✔
299

300
  @Override
301
  public void terminateUpload(UploadInfo info) throws UploadNotFoundException, IOException {
302
    if (info != null) {
4✔
303
      if (info.getDuplicatesUploadId() == null
8!
304
          && info.getChecksum() != null
6✔
305
          && info.getChecksumAlgorithm() != null) {
4!
306
        Path checksumFile = getChecksumPath(info.getChecksum(), info.getChecksumAlgorithm());
14✔
307
        Files.deleteIfExists(checksumFile);
6✔
308
      }
309
      Path uploadPath = getPathInStorageDirectory(info.getId());
10✔
310
      FileUtils.deleteDirectory(uploadPath.toFile());
6✔
311
    }
312
  }
2✔
313

314
  @Override
315
  public Long getUploadExpirationPeriod() {
316
    return uploadExpirationPeriod;
6✔
317
  }
318

319
  @Override
320
  public void setUploadExpirationPeriod(Long uploadExpirationPeriod) {
321
    this.uploadExpirationPeriod = uploadExpirationPeriod;
3✔
322
  }
1✔
323

324
  @Override
325
  public void setUploadConcatenationService(UploadConcatenationService concatenationService) {
326
    Objects.requireNonNull(concatenationService);
6✔
327
    this.uploadConcatenationService = concatenationService;
6✔
328
  }
2✔
329

330
  @Override
331
  public UploadConcatenationService getUploadConcatenationService() {
332
    return uploadConcatenationService;
3✔
333
  }
334

335
  @Override
336
  public InputStream getUploadedBytes(String uploadUri, String ownerKey)
337
      throws IOException, UploadNotFoundException {
338

339
    UploadId id = idFactory.readUploadId(uploadUri);
10✔
340

341
    UploadInfo uploadInfo = getUploadInfo(id);
8✔
342
    if (uploadInfo == null || !Objects.equals(uploadInfo.getOwnerKey(), ownerKey)) {
14!
343
      throw new UploadNotFoundException(
8✔
344
          "The upload with id " + id + " could not be found for owner " + ownerKey);
345
    } else {
346
      return getUploadedBytes(id);
8✔
347
    }
348
  }
349

350
  @Override
351
  public InputStream getUploadedBytes(UploadId id) throws IOException, UploadNotFoundException {
352
    InputStream inputStream = null;
4✔
353
    UploadInfo uploadInfo = getUploadInfo(id);
8✔
354
    if (uploadInfo == null) {
4✔
355
      throw new UploadNotFoundException("The upload with id " + id + " was not found.");
7✔
356
    }
357

358
    if (uploadInfo.getDuplicatesUploadId() != null) {
6✔
359
      return getUploadedBytes(uploadInfo.getDuplicatesUploadId());
5✔
360
    }
361

362
    if (UploadType.CONCATENATED.equals(uploadInfo.getUploadType())
13!
363
        && uploadConcatenationService != null) {
364
      inputStream = uploadConcatenationService.getConcatenatedBytes(uploadInfo);
6✔
365

366
    } else {
367
      Path bytesPath = getBytesPath(id);
8✔
368
      // If bytesPath is not null, we know this is a valid Upload URI
369
      if (bytesPath != null) {
4!
370
        if (!Files.exists(bytesPath)) {
10!
UNCOV
371
          throw new UploadNotFoundException(
×
372
              "The upload bytes for id " + id + " could not be found.");
373
        }
374
        inputStream = Channels.newInputStream(FileChannel.open(bytesPath, READ));
20✔
375
      }
376
    }
377

378
    return inputStream;
4✔
379
  }
380

381
  @Override
382
  public void copyUploadTo(UploadInfo info, OutputStream outputStream)
383
      throws UploadNotFoundException, IOException {
384

385
    List<UploadInfo> uploads = getUploads(info);
8✔
386

387
    try (WritableByteChannel outputChannel = Channels.newChannel(outputStream)) {
6✔
388

389
      for (UploadInfo upload : uploads) {
20✔
390
        if (upload == null) {
4!
UNCOV
391
          log.warn("We cannot copy the bytes of an upload that does not exist");
×
392

393
        } else if (upload.isUploadInProgress()) {
6!
UNCOV
394
          log.warn(
×
395
              "We cannot copy the bytes of upload {} because it is still in progress",
UNCOV
396
              upload.getId());
×
397

398
        } else {
399
          UploadId readId =
400
              upload.getDuplicatesUploadId() != null
6✔
401
                  ? upload.getDuplicatesUploadId()
6✔
402
                  : upload.getId();
6✔
403
          Path bytesPath = getBytesPath(readId);
8✔
404
          if (!Files.exists(bytesPath)) {
10!
UNCOV
405
            throw new UploadNotFoundException(
×
406
                "The upload bytes for id " + readId + " could not be found.");
407
          }
408
          try (FileChannel file = FileChannel.open(bytesPath, READ)) {
18✔
409
            // Efficiently copy the bytes to the output stream
410
            file.transferTo(0, upload.getLength(), outputChannel);
16✔
411
          }
412
        }
413
      }
2✔
414
    }
415
  }
2✔
416

417
  @Override
418
  public void cleanupExpiredUploads(UploadLockingService uploadLockingService) throws IOException {
419
    try (DirectoryStream<Path> expiredUploadsStream =
2✔
420
        Files.newDirectoryStream(
4✔
421
            getStoragePath(), new ExpiredUploadFilter(this, uploadLockingService))) {
12✔
422

423
      for (Path path : expiredUploadsStream) {
20✔
424
        FileUtils.deleteDirectory(path.toFile());
6✔
425
      }
2✔
426
    }
427
  }
2✔
428

429
  private List<UploadInfo> getUploads(UploadInfo info) throws IOException, UploadNotFoundException {
430
    List<UploadInfo> uploads;
431

432
    if (info != null
8!
433
        && UploadType.CONCATENATED.equals(info.getUploadType())
9!
434
        && uploadConcatenationService != null) {
435
      uploadConcatenationService.merge(info);
4✔
436
      uploads = uploadConcatenationService.getPartialUploads(info);
6✔
437
    } else {
438
      uploads = Collections.singletonList(info);
6✔
439
    }
440
    return uploads;
4✔
441
  }
442

443
  private Path getBytesPath(UploadId id) throws UploadNotFoundException {
444
    return getPathInUploadDir(id, DATA_FILE);
10✔
445
  }
446

447
  private Path getInfoPath(UploadId id) throws UploadNotFoundException {
448
    return getPathInUploadDir(id, INFO_FILE);
10✔
449
  }
450

451
  private Path getChecksumPath(String checksum, ChecksumAlgorithm algorithm) {
452
    return getStoragePath()
6✔
453
        .getParent()
4✔
454
        .resolve("checksums")
4✔
455
        .resolve(algorithm.toString())
6✔
456
        .resolve(checksum);
2✔
457
  }
458

459
  private Path createUploadDirectory(UploadId id) throws IOException {
460
    return Files.createDirectories(getPathInStorageDirectory(id));
14✔
461
  }
462

463
  private Path getPathInUploadDir(UploadId id, String fileName) throws UploadNotFoundException {
464
    // Get the upload directory
465
    Path uploadDir = getPathInStorageDirectory(id);
8✔
466
    if (uploadDir != null && Files.exists(uploadDir)) {
14✔
467
      return uploadDir.resolve(fileName);
8✔
468
    } else {
469
      throw new UploadNotFoundException("The upload for id " + id + " was not found.");
14✔
470
    }
471
  }
472

473
  private synchronized UploadId createNewId() throws IOException {
474
    UploadId id;
475
    do {
476
      id = idFactory.createId();
8✔
477
      // For extra safety, double check that this ID is not in use yet
478
    } while (getUploadInfo(id) != null);
8!
479
    return id;
4✔
480
  }
481

482
  private long writeAsMuchAsPossible(FileChannel file) throws IOException {
483
    long offset = 0;
4✔
484
    if (file != null) {
4!
485
      file.force(true);
6✔
486
      offset = file.size();
6✔
487
    }
488
    return offset;
4✔
489
  }
490
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc