diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx index 7c77a57edc..819251cc2b 100644 --- a/app/actions/definitions/documents.tsx +++ b/app/actions/definitions/documents.tsx @@ -38,6 +38,7 @@ import Icon from "@shared/components/Icon"; import type { NavigationNode } from "@shared/types"; import { ExportContentType, TeamPreference } from "@shared/types"; import { getEventFiles } from "@shared/utils/files"; +import { Week } from "@shared/utils/time"; import type UserMembership from "~/models/UserMembership"; import { client } from "~/utils/ApiClient"; import DocumentDelete from "~/scenes/DocumentDelete"; @@ -630,7 +631,7 @@ export const copyDocumentAsMarkdown = createAction({ if (document) { const res = await client.post("/documents.export", { id: document.id, - signedUrls: 3600 * 24 * 30, // 30 days + signedUrls: Week.seconds, // 7 days (AWS S3 max for presigned URLs) }); copy(res.data); toast.success(t("Markdown copied to clipboard")); diff --git a/server/storage/files/BaseStorage.ts b/server/storage/files/BaseStorage.ts index c947aa436b..980cb872d9 100644 --- a/server/storage/files/BaseStorage.ts +++ b/server/storage/files/BaseStorage.ts @@ -4,6 +4,7 @@ import type { PresignedPost } from "@aws-sdk/s3-presigned-post"; import omit from "lodash/omit"; import FileHelper from "@shared/editor/lib/FileHelper"; import { isBase64Url, isInternalUrl } from "@shared/utils/urls"; +import { Week } from "@shared/utils/time"; import env from "@server/env"; import Logger from "@server/logging/Logger"; import type { RequestInit } from "@server/utils/fetch"; @@ -14,6 +15,12 @@ export default abstract class BaseStorage { /** The default number of seconds until a signed URL expires. */ public static defaultSignedUrlExpires = 300; + /** + * The maximum number of seconds until a signed URL expires for S3 Signature V4. + * AWS S3 Signature V4 presigned URLs must have an expiration date less than one week in the future. + */ + public static maxSignedUrlExpires = Week.seconds; + /** * Returns a presigned post for uploading files to the storage provider. * diff --git a/server/storage/files/S3Storage.test.ts b/server/storage/files/S3Storage.test.ts new file mode 100644 index 0000000000..8cb8e4e184 --- /dev/null +++ b/server/storage/files/S3Storage.test.ts @@ -0,0 +1,40 @@ +import { Week, Day } from "@shared/utils/time"; +import BaseStorage from "./BaseStorage"; + +describe("S3Storage", () => { + describe("getSignedUrl expiration limits", () => { + it("should define maximum expiration as 7 days for AWS S3 Signature V4", () => { + // AWS S3 Signature V4 presigned URLs have a maximum expiration of 7 days + const maxExpiration = Week.seconds; + + // Verify our constant matches AWS limit + expect(BaseStorage.maxSignedUrlExpires).toBe(maxExpiration); + expect(BaseStorage.maxSignedUrlExpires).toBe(604800); // 7 days in seconds + }); + + it("should have Week.seconds equal to 7 days", () => { + expect(Week.seconds).toBe(7 * 24 * 60 * 60); + expect(Week.seconds).toBe(604800); + }); + + it("should ensure 30 days exceeds the limit", () => { + const thirtyDays = 30 * Day.seconds; + expect(thirtyDays).toBeGreaterThan(BaseStorage.maxSignedUrlExpires); + expect(thirtyDays).toBe(2592000); // 30 days in seconds + }); + + it("should ensure 4 days is within the limit", () => { + const fourDays = 4 * Day.seconds; + expect(fourDays).toBeLessThan(BaseStorage.maxSignedUrlExpires); + expect(fourDays).toBe(345600); // 4 days in seconds + }); + + it("should clamp values that exceed the limit", () => { + const thirtyDays = 30 * Day.seconds; + const clampedValue = Math.min(thirtyDays, BaseStorage.maxSignedUrlExpires); + + expect(clampedValue).toBe(BaseStorage.maxSignedUrlExpires); + expect(clampedValue).toBe(Week.seconds); + }); + }); +}); diff --git a/server/storage/files/S3Storage.ts b/server/storage/files/S3Storage.ts index a31192a593..53ca4092a7 100644 --- a/server/storage/files/S3Storage.ts +++ b/server/storage/files/S3Storage.ts @@ -152,8 +152,11 @@ export default class S3Storage extends BaseStorage { if (isDocker) { return `${this.getPublicEndpoint()}/${key}`; } else { + // Ensure expiration does not exceed AWS S3 Signature V4 limit of 7 days + const clampedExpiresIn = Math.min(expiresIn, S3Storage.maxSignedUrlExpires); + const command = new GetObjectCommand(params); - const url = await getSignedUrl(this.client, command, { expiresIn }); + const url = await getSignedUrl(this.client, command, { expiresIn: clampedExpiresIn }); if (env.AWS_S3_ACCELERATE_URL) { return url.replace( diff --git a/shared/utils/time.ts b/shared/utils/time.ts index ae8ac5a489..a1c8a28732 100644 --- a/shared/utils/time.ts +++ b/shared/utils/time.ts @@ -27,3 +27,14 @@ export class Day { /** Minutes in a day */ public static minutes = 24 * Hour.minutes; } + +export class Week { + /** Milliseconds in a week */ + public static ms = 7 * Day.ms; + /** Seconds in a week */ + public static seconds = 7 * Day.seconds; + /** Minutes in a week */ + public static minutes = 7 * Day.minutes; + /** Days in a week */ + public static days = 7; +}