From d9877bc345817e8b43e4c91c5bc2bab516032ace Mon Sep 17 00:00:00 2001 From: Markus Horst Becker Date: Mon, 14 Mar 2022 15:18:58 +0100 Subject: [PATCH] Use 'Expires' header to time-limit URLs --- source/image-handler/image-request.ts | 26 ++++++++++++ .../image-handler/test/image-request.spec.ts | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/source/image-handler/image-request.ts b/source/image-handler/image-request.ts index 54b6fd901..049f744e9 100644 --- a/source/image-handler/image-request.ts +++ b/source/image-handler/image-request.ts @@ -41,6 +41,7 @@ export class ImageRequest { imageRequestInfo = { ...imageRequestInfo, ...originalImage }; imageRequestInfo.headers = this.parseImageHeaders(event, imageRequestInfo.requestType); + this.validateRequestExpires(imageRequestInfo); // If the original image is SVG file and it has any edits but no output format, change the format to WebP. if (imageRequestInfo.contentType === 'image/svg+xml' && imageRequestInfo.edits && Object.keys(imageRequestInfo.edits).length > 0 && !imageRequestInfo.edits.toFormat) { @@ -443,4 +444,29 @@ export class ImageRequest { } } } + + private validateRequestExpires(requestInfo: ImageRequestInfo): void { + try { + const expires = requestInfo.headers?.expires; + if (expires !== undefined) { + const parsedDate = new Date(expires); + if (isNaN(parsedDate.getTime())) { + throw new ImageHandlerError(StatusCodes.BAD_REQUEST, 'ImageRequestExpiryFormat', 'Request has invalid expiry date.'); + } + const now = new Date(); + if (now > parsedDate) { + throw new ImageHandlerError(StatusCodes.FORBIDDEN, 'ImageRequestExpired', 'Request has expired.'); + } + } + } catch (error) { + if (error.code === 'ImageRequestExpired') { + throw error; + } + if (error.code === 'ImageRequestExpiryFormat') { + throw error; + } + console.error('Error occurred while checking expiry.', error); + throw new ImageHandlerError(StatusCodes.INTERNAL_SERVER_ERROR, 'ExpiryDateCheckFailure', 'Expiry date check failed.'); + } + } } diff --git a/source/image-handler/test/image-request.spec.ts b/source/image-handler/test/image-request.spec.ts index c7535c528..344e40100 100644 --- a/source/image-handler/test/image-request.spec.ts +++ b/source/image-handler/test/image-request.spec.ts @@ -853,6 +853,46 @@ describe('setup()', () => { expect(imageRequestInfo).toEqual(expectedResult); }); }); + + describe('011/expiryDate', () => { + it.each([ + { + path: '/eyJidWNrZXQiOiJ0ZXN0IiwicmVxdWVzdFR5cGUiOiJEZWZhdWx0Iiwia2V5IjoidGVzdC5wbmciLCJoZWFkZXJzIjp7ImV4cGlyZXMiOiJUaHUsIDAxIEphbiAxOTcwIDAwOjAwOjAwIEdNVCJ9fQ==', + error: { + code: 'ImageRequestExpired', + message: 'Request has expired.', + status: StatusCodes.FORBIDDEN, + }, + }, + { + path: '/eyJidWNrZXQiOiJ0ZXN0IiwicmVxdWVzdFR5cGUiOiJEZWZhdWx0Iiwia2V5IjoidGVzdC5wbmciLCJoZWFkZXJzIjp7ImV4cGlyZXMiOiJpbnZhbGlkS2V5In19', + error: { + code: 'ImageRequestExpiryFormat', + message: 'Request has invalid expiry date.', + status: StatusCodes.BAD_REQUEST, + } + } + ])("Should throw an error when $error.message", (async ({ path, error: expectedError }) => { + // Arrange + const event = { + path, + }; + // Mock + mockAwsS3.getObject.mockImplementationOnce(() => ({ + promise() { + return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') }); + } + })); + // Act + const imageRequest = new ImageRequest(s3Client, secretProvider); + try { + await imageRequest.setup(event); + } catch (error) { + // Assert + expect(error).toMatchObject(expectedError); + } + })); + }); }); describe('getOriginalImage()', () => {