فهرست منبع

Set content disposition according to allowlist or content type

arvid-e 9 ماه پیش
والد
کامیت
6c8da22f1f
1فایلهای تغییر یافته به همراه33 افزوده شده و 4 حذف شده
  1. 33 4
      apps/app/src/server/service/file-uploader/utils/headers.ts

+ 33 - 4
apps/app/src/server/service/file-uploader/utils/headers.ts

@@ -3,6 +3,8 @@ import type { Response } from 'express';
 import type { ExpressHttpHeader, IContentHeaders } from '~/server/interfaces/attachment';
 import type { IAttachmentDocument } from '~/server/models/attachment';
 
+import { INLINE_ALLOWLIST_MIME_TYPES } from './security'; // Adjust path if necessary
+
 
 export class ContentHeaders implements IContentHeaders {
 
@@ -14,22 +16,48 @@ export class ContentHeaders implements IContentHeaders {
 
   contentDisposition?: ExpressHttpHeader<'Content-Disposition'>;
 
+  xContentTypeOptions?: ExpressHttpHeader<'X-Content-Type-Options'>;
+
   constructor(attachment: IAttachmentDocument, opts?: {
     inline?: boolean,
   }) {
+    const attachmentContentType = attachment.fileFormat; // Stored Content-Type
+    const filename = attachment.originalName; // Original filename
+
+    // Define the final content type value in a local variable.
+    const actualContentTypeString: string = attachmentContentType || 'application/octet-stream';
 
     this.contentType = {
       field: 'Content-Type',
-      value: attachment.fileFormat,
+      value: actualContentTypeString,
     };
+
+    // Determine Content-Disposition based on allowlist and the 'inline' request flag
+    const requestedInline = opts?.inline ?? false;
+
+    // Content should be inline ONLY IF:
+    // a) It was requested as inline AND
+    // b) Its MIME type (using the *guaranteed string* local variable) is explicitly in our security allowlist.
+    const shouldBeInline = requestedInline && INLINE_ALLOWLIST_MIME_TYPES.has(actualContentTypeString);
+
+    this.contentDisposition = {
+      field: 'Content-Disposition',
+      // If actuallyShouldBeInline is true, set to inline; otherwise, force attachment with filename
+      value: shouldBeInline
+        ? 'inline'
+        : `attachment;filename*=UTF-8''${encodeURIComponent(filename)}`,
+    };
+
     this.contentSecurityPolicy = {
       field: 'Content-Security-Policy',
       // eslint-disable-next-line max-len
       value: "script-src 'unsafe-hashes'; style-src 'self' 'unsafe-inline'; object-src 'none'; require-trusted-types-for 'script'; media-src 'self'; default-src 'none';",
     };
-    this.contentDisposition = {
-      field: 'Content-Disposition',
-      value: `${opts?.inline ? 'inline' : 'attachment'};filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
+
+    // Always set X-Content-Type-Options: nosniff
+    this.xContentTypeOptions = {
+      field: 'X-Content-Type-Options',
+      value: 'nosniff',
     };
 
     if (attachment.fileSize) {
@@ -49,6 +77,7 @@ export class ContentHeaders implements IContentHeaders {
       this.contentLength,
       this.contentSecurityPolicy,
       this.contentDisposition,
+      this.xContentTypeOptions,
     ]
       // exclude undefined
       .filter((member): member is NonNullable<typeof member> => member != null);