From 9a2bc67b1d0079dafd7062dcc0f06702563b8370 Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Tue, 27 Oct 2026 15:30:03 +0100 Subject: [PATCH] Reject expired uploads on ZK and hotlink GET with 404. Use dayjs to compare expiresAt before reading storage or incrementing view_count. --- .../upload/services/UploadService.spec.ts | 31 +++++++++++++++++++ .../modules/upload/services/UploadService.ts | 9 ++++++ 2 files changed, 40 insertions(+) diff --git a/backend/src/modules/upload/services/UploadService.spec.ts b/backend/src/modules/upload/services/UploadService.spec.ts index 3ba9041..7060632 100644 --- a/backend/src/modules/upload/services/UploadService.spec.ts +++ b/backend/src/modules/upload/services/UploadService.spec.ts @@ -302,6 +302,24 @@ describe('UploadService', () => { await expect(service.getZkUploadCiphertext(fixedId)).rejects.toThrow(NotFoundException); expect(uploadRepository.increment).not.toHaveBeenCalled(); }); + + it('throws NotFoundException when expiresAt is in the past', async () => { + uploadRepository.findOne.mockResolvedValue({ + storagePath: fixedId, + iv: Buffer.alloc(12, 0x11), + expiresAt: new Date(Date.now() - 60_000), + zk: { + passwordProtected: false, + wrappedDek: null, + wrapIv: null, + salt: null + } + }); + + await expect(service.getZkUploadCiphertext(fixedId)).rejects.toThrow(NotFoundException); + expect(storageService.read).not.toHaveBeenCalled(); + expect(uploadRepository.increment).not.toHaveBeenCalled(); + }); }); describe('getHotlinkImage', () => { @@ -364,6 +382,19 @@ describe('UploadService', () => { await expect(service.getHotlinkImage(fixedId, secret)).rejects.toThrow(NotFoundException); expect(uploadRepository.increment).not.toHaveBeenCalled(); }); + + it('throws NotFoundException when expiresAt is in the past', async () => { + uploadRepository.findOne.mockResolvedValue({ + storagePath: fixedId, + iv, + expiresAt: new Date(Date.now() - 60_000), + hotlink: { mimeType: 'image/png' } + }); + + await expect(service.getHotlinkImage(fixedId, secret)).rejects.toThrow(NotFoundException); + expect(storageService.read).not.toHaveBeenCalled(); + expect(uploadRepository.increment).not.toHaveBeenCalled(); + }); }); describe('deleteUpload', () => { diff --git a/backend/src/modules/upload/services/UploadService.ts b/backend/src/modules/upload/services/UploadService.ts index 27c5b8a..30c89a7 100644 --- a/backend/src/modules/upload/services/UploadService.ts +++ b/backend/src/modules/upload/services/UploadService.ts @@ -1,5 +1,6 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import dayjs from 'dayjs'; import { fileTypeFromBuffer } from 'file-type'; import { createHash, randomBytes, randomUUID } from 'node:crypto'; import { DataSource, Repository } from 'typeorm'; @@ -127,6 +128,10 @@ export class UploadService { throw new NotFoundException(); } + if (upload.expiresAt && dayjs(upload.expiresAt).isBefore(dayjs())) { + throw new NotFoundException(); + } + let ciphertextBuffer: Buffer; try { @@ -170,6 +175,10 @@ export class UploadService { throw new NotFoundException(); } + if (upload.expiresAt && dayjs(upload.expiresAt).isBefore(dayjs())) { + throw new NotFoundException(); + } + let ciphertextBuffer: Buffer; try {