Skip to content

Conversation

@jocelynlin-wd
Copy link

@jocelynlin-wd jocelynlin-wd commented Dec 18, 2025

Address security vulnerability reported in https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-j8g8-j7fc-43v6 (also in issues #5579)

Add MIME type and extension validation for file uploads:

  • Updated jest.config.js to include the 'src' directory in test roots.
  • Introduced a new validator for checking MIME type and file extension matches, addressing security vulnerabilities.
  • clean up invalid binary file in storage when loader failed (leverage loader to verify content)
  • Implemented validation in multiple server controllers and utilities to prevent MIME type spoofing attacks.
  • Added comprehensive tests for the new validation logic in validator.test.ts.

Unit Tests

 PASS  src/validator.test.ts (12.537 s)
  validateMimeTypeAndExtensionMatch
    valid cases
      ✓ should pass validation for matching MIME type and extension - document.txt with text/plain (3 ms)
      ✓ should pass validation for matching MIME type and extension - page.html with text/html
      ✓ should pass validation for matching MIME type and extension - data.json with application/json
      ✓ should pass validation for matching MIME type and extension - document.pdf with application/pdf (1 ms)
      ✓ should pass validation for matching MIME type and extension - script.js with text/javascript
      ✓ should pass validation for matching MIME type and extension - script.js with application/javascript
      ✓ should pass validation for matching MIME type and extension - readme.md with text/markdown
      ✓ should pass validation for matching MIME type and extension - readme.md with text/x-markdown
      ✓ should pass validation for matching MIME type and extension - DOCUMENT.TXT with text/plain (1 ms)
      ✓ should pass validation for matching MIME type and extension - Document.TxT with text/plain
      ✓ should pass validation for matching MIME type and extension - my.document.txt with text/plain
    invalid filename
      ✓ should throw error for empty filename (12 ms)
      ✓ should throw error for null filename (1 ms)
      ✓ should throw error for undefined filename (5 ms)
      ✓ should throw error for non-string filename (number)
      ✓ should throw error for object filename
    invalid MIME type
      ✓ should throw error for empty MIME type
      ✓ should throw error for null MIME type
      ✓ should throw error for undefined MIME type
      ✓ should throw error for non-string MIME type (number)
    path traversal detection
      ✓ should throw error for filename with ..
      ✓ should throw error for filename with .. in middle
      ✓ should throw error for filename with multle levels of .. (1 ms)
      ✓ should throw error for filename with  ..\..\..
      ✓ should throw error for filename with ....//....//
      ✓ should throw error for filename starting with /
      ✓ should throw error for Windows absolute path
      ✓ should throw error for URL encoded path traversal
      ✓ should throw error for URL encoded path traversal multiple levels
      ✓ should throw error for null byte
    files without extensions
      ✓ should throw error for filename without extension
      ✓ should throw error for filename ending with dot
    unsupported MIME types
      ✓ should throw error for unsupported MIME type application/octet-stream with file.txt (1 ms)
      ✓ should throw error for unsupported MIME type invalid-mime-type with file.txt
      ✓ should throw error for unsupported MIME type application/x-msdownload with malware.exe
      ✓ should throw error for unsupported MIME type application/x-executable with script.exe
      ✓ should throw error for unsupported MIME type application/x-msdownload with program.EXE
      ✓ should throw error for unsupported MIME type application/octet-stream with script.js
    MIME type and extension mismatches
      ✓ should throw error when extension does not match MIME type - file.txt with application/json (1 ms)
      ✓ should throw error when extension does not match MIME type - script.js with application/pdf
      ✓ should throw error when extension does not match MIME type - page.html with text/plain
      ✓ should throw error when extension does not match MIME type - document.pdf with application/json
      ✓ should throw error when extension does not match MIME type - data.json with text/plain
      ✓ should throw error when extension does not match MIME type - malware.exe with text/plain (1 ms)
      ✓ should throw error when extension does not match MIME type - script.js with application/json

Manual tests

----- Test Parameters: ----------------------------------------
  filename:     security-test.js
  mimetype:     application/javascript
  chat_target:  test-js-ext
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-js-ext -F [email protected];type=application/javascript

{"statusCode":500,"success":false,"message":"Error: attachmentService.createAttachment - File type 'application/javascript' is not allowed. Allowed types: text/css, text/csv, text/html, application/json, text/markdown, application/x-yaml, application/pdf, application/sql, text/plain, application/xml, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.openxmlformats-officedocument.presentationml.presentation","stack":{}} 
=> Expected: Validation Error

----- Test Parameters: ----------------------------------------
  filename:     security-test.js
  mimetype:     application/pdf
  chat_target:  test-js-ext
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-js-ext -F [email protected];type=application/pdf

{"statusCode":500,"success":false,"message":"Error: attachmentService.createAttachment - MIME type mismatch: file extension \"js\" does not match declared MIME type \"application/pdf\". Expected: pdf","stack":{}} 
=> Expected: Validation Error

----- Test Parameters: ----------------------------------------
  filename:     security-test-text.txt
  mimetype:     text/plain
  chat_target:  test-good-ext
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-good-ext -F [email protected];type=text/plain

[{"name":"security-test-text.txt","mimeType":"text/plain","size":110,"content":"alert(\"Hello, world! You are hacked\");\nconsole.log(\"🔥🔥🔥 Hello, world! You are hacked 🔥🔥🔥\");\n"}] 
=> Expected: SUCCESS

----- Test Parameters: ----------------------------------------
  filename:     security-test.md
  mimetype:     text/markdown
  chat_target:  test-good-ext
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-good-ext -F [email protected];type=text/markdown

[{"name":"security-test.md","mimeType":"text/markdown","size":111,"content":"alert(\"Hello, world! You are hacked\");\nconsole.log(\"🔥🔥🔥 Hello, world! You are hacked 🔥🔥🔥\");\n\n"}] 
=> Expected: SUCCESS

----- Test Parameters: ----------------------------------------
  filename:     security-test.pdf
  mimetype:     application/pdf
  chat_target:  test-good-ext
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-good-ext -F [email protected];type=application/pdf

[{"name":"security-test.pdf","mimeType":"application/pdf","size":8991,"content":""}] 
=> Expected: Success

----- Test Parameters: ----------------------------------------
  filename:     security-test.png
  mimetype:     image/png
  chat_target:  test-image
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-image -F [email protected];type=image/png

{"statusCode":500,"success":false,"message":"Error: attachmentService.createAttachment - File type 'image/png' is not allowed. Allowed types: text/css, text/csv, text/html, application/json, text/markdown, application/x-yaml, application/pdf, application/sql, text/plain, application/xml, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.openxmlformats-officedocument.presentationml.presentation","stack":{}} 
=> Expected: Unsupported Error

----- Test Parameters: ----------------------------------------
  filename:     security-test-faker.pdf
  mimetype:     application/pdf
  chat_target:  test-fake-ext
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-fake-ext -F [email protected];type=application/pdf

{"statusCode":500,"success":false,"message":"Error: attachmentService.createAttachment - Failed createFileAttachment: security-test-faker.pdf (application/pdf - Invalid PDF structure","stack":{}} 
=> Expected: Invalid File Error

----- Test Parameters: ----------------------------------------
  filename:     security-test-faker.docx
  mimetype:     application/vnd.openxmlformats-officedocument.wordprocessingml.document
  chat_target:  test-fake-ext
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-fake-ext -F [email protected];type=application/vnd.openxmlformats-officedocument.wordprocessingml.document

{"statusCode":500,"success":false,"message":"Error: attachmentService.createAttachment - Failed createFileAttachment: security-test-faker.docx (application/vnd.openxmlformats-officedocument.wordprocessingml.document - Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html","stack":{}} 
=> Expected: Invalid File Error

----- Test Parameters: ----------------------------------------
  filename:     security-test-faker.doc
  mimetype:     application/msword
  chat_target:  test-fake-ext
  curl_cmd:  curl -s -S -X POST http://localhost:3000/api/v1/attachments/a29bbc33-c530-4981-b3ef-58a5aea28769/test-fake-ext -F [email protected];type=application/msword

{"statusCode":500,"success":false,"message":"Error: attachmentService.createAttachment - Failed createFileAttachment: security-test-faker.doc (application/msword - Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html","stack":{}} 
=> Expected: Invalid File Error

----- Flowise Storage Structure after running above ----------------------------------------
.
|____a29bbc33-c530-4981-b3ef-58a5aea28769
| |____2e849e0e-721b-4ef8-9e38-cc739ff40928
| | |____security-test-text.txt
| |____test-fake-ext
| |____test-good-ext
| | |____security-test.pdf
| | |____security-test.md
| | |____security-test-text.txt
|____docustore
| |____576cad4a-813f-4af5-bb22-276f04fb1ee7
| | |____2025-1215-security-test.js

- Updated jest.config.js to include the 'src' directory in test roots.
- Introduced a new validator for checking MIME type and file extension matches, addressing security vulnerabilities.
- Implemented validation in multiple server controllers and utilities to prevent MIME type spoofing attacks.
- Added comprehensive tests for the new validation logic in validator.test.ts.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @jocelynlin-wd, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security of file uploads by introducing a comprehensive validation mechanism. It directly addresses a reported vulnerability (GHSA-j8g8-j7fc-43v6) by preventing MIME type spoofing and path traversal attacks. The new validation ensures that uploaded files have appropriate extensions matching their declared MIME types and are free from malicious path components. This logic is integrated across several file handling services, and a cleanup process is added to remove invalid files from storage, bolstering the system's integrity against malicious uploads.

Highlights

  • Security Vulnerability Fix: Addresses the reported security vulnerability GHSA-j8g8-j7fc-43v6 by implementing strict file upload validation.
  • MIME Type and Extension Validation: Introduces a new validator to ensure that uploaded file extensions correctly match their declared MIME types, preventing spoofing attacks.
  • Path Traversal Prevention: Includes robust checks within the new validation logic to detect and block path traversal attempts in filenames.
  • Cleanup on Failed Uploads: Adds logic to automatically remove partially uploaded or invalid files from storage if subsequent processing by a loader fails.
  • Extensive Test Coverage: New unit tests (validator.test.ts) have been added to cover a wide range of valid and invalid scenarios for file and MIME type validation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses a security vulnerability by introducing robust MIME type and file extension validation for file uploads. The new validator is well-implemented and comes with a comprehensive suite of tests, covering many edge cases. The validation has been consistently applied across all relevant parts of the application that handle file uploads. Additionally, the new logic to clean up files from storage when a loader fails is a great improvement for system hygiene. I have a few minor suggestions to improve code consistency and error message accuracy.

jocelynlin-wd and others added 3 commits December 17, 2025 21:15
isUnsafeFilePath function checks for various security risks, including absolute paths, null bytes, and control characters, not just path traversal. A more general error message would be more appropriate to cover all cases.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/nodes'],
roots: ['<rootDir>/nodes', '<rootDir>/src'],
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests in components/src was never run, need to expose the directory as roots

jest.mock('@opentelemetry/exporter-trace-otlp-proto', () => {
return {
ProtoOTLPTraceExporter: jest.fn().mockImplementation((args) => {
OTLPTraceExporter: jest.fn().mockImplementation((args) => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the exported function from the library is OTLPTraceExporter not the renamed version import { OTLPTraceExporter as ProtoOTLPTraceExporter }.

}
})

import { OTLPTraceExporter as ProtoOTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move import to the top of the file

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment

OS: WIN
Node: v20.19.1

Result

 FAIL  src/validator.test.ts (15.632 s)
  validateMimeTypeAndExtensionMatch
    valid cases
      √ should pass validation for matching MIME type and extension - document.txt with text/plain (9 ms)
      √ should pass validation for matching MIME type and extension - page.html with text/html
      √ should pass validation for matching MIME type and extension - data.json with application/json
      √ should pass validation for matching MIME type and extension - document.pdf with application/pdf (1 ms)
      √ should pass validation for matching MIME type and extension - script.js with text/javascript
      √ should pass validation for matching MIME type and extension - script.js with application/javascript (1 ms)
      √ should pass validation for matching MIME type and extension - readme.md with text/markdown
      √ should pass validation for matching MIME type and extension - readme.md with text/x-markdown
      √ should pass validation for matching MIME type and extension - DOCUMENT.TXT with text/plain
      √ should pass validation for matching MIME type and extension - Document.TxT with text/plain
      √ should pass validation for matching MIME type and extension - my.document.txt with text/plain
    invalid filename
      √ should throw error for empty filename (11 ms)
      √ should throw error for null filename (1 ms)
      √ should throw error for undefined filename (1 ms)
      √ should throw error for non-string filename (number)
      √ should throw error for object filename (1 ms)
    invalid MIME type
      √ should throw error for empty MIME type
      √ should throw error for null MIME type
      √ should throw error for undefined MIME type
      √ should throw error for non-string MIME type (number) (1 ms)
    path traversal detection
      × should throw error for filename with .. (13 ms)
      × should throw error for filename with .. in middle (3 ms)
      × should throw error for filename with multle levels of .. (2 ms)
      × should throw error for filename with  ..\..\.. (2 ms)
      × should throw error for filename with ....//....// (1 ms)
      × should throw error for filename starting with / (1 ms)
      × should throw error for Windows absolute path (2 ms)
      × should throw error for URL encoded path traversal (1 ms)
      × should throw error for URL encoded path traversal multiple levels (2 ms)
      × should throw error for null byte (2 ms)
    files without extensions
      √ should throw error for filename without extension
      √ should throw error for filename ending with dot (1 ms)
    unsupported MIME types
      √ should throw error for unsupported MIME type application/octet-stream with file.txt
      √ should throw error for unsupported MIME type invalid-mime-type with file.txt (1 ms)
      √ should throw error for unsupported MIME type application/x-msdownload with malware.exe
      √ should throw error for unsupported MIME type application/x-executable with script.exe
      √ should throw error for unsupported MIME type application/x-msdownload with program.EXE (1 ms)
      √ should throw error for unsupported MIME type application/octet-stream with script.js
    MIME type and extension mismatches
      √ should throw error when extension does not match MIME type - file.txt with application/json
      √ should throw error when extension does not match MIME type - script.js with application/pdf (1 ms)
      √ should throw error when extension does not match MIME type - page.html with text/plain
      √ should throw error when extension does not match MIME type - document.pdf with application/json (1 ms)
      √ should throw error when extension does not match MIME type - data.json with text/plain
      √ should throw error when extension does not match MIME type - malware.exe with text/plain (1 ms)
      √ should throw error when extension does not match MIME type - script.js with application/json

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for filename with ..

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"../file.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"../file.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for filename with .. in middle

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"path/../file.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"path/../file.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for filename with multle levels of ..

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"../../../etc/passwd.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"../../../etc/passwd.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for filename with  ..\..\..

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"..\\..\\..\\windows\\system32\\file.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"..\\..\\..\\windows\\system32\\file.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for filename with ....//....//

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"....//....//etc/passwd.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"....//....//etc/passwd.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for filename starting with /

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"/etc/passwd.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"/etc/passwd.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for Windows absolute path

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"C:\\file.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"C:\\file.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for URL encoded path traversal

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"%2e%2e/file.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"%2e%2e/file.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for URL encoded path traversal multiple levels

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"%2e%2e%2f%2e%2e%2f%2e%2e%2ffile.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"%2e%2e%2f%2e%2e%2f%2e%2e%2ffile.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

  ● validateMimeTypeAndExtensionMatch › path traversal detection › should throw error for null byte

    expect(received).toThrow(expected)

    Expected substring: "Invalid filename: path traversal detected in filename \"file.txt\""
    Received message:   "Invalid filename: unsafe characters or path traversal attempt detected in filename \"file.txt\""

          83 |     }
          84 |     if (isUnsafeFilePath(filename)) {
        > 85 |         throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
             |               ^
          86 |     }
          87 | }
          88 |

          at validateFilename (src/validator.ts:85:15)
          at validateMimeTypeAndExtensionMatch (src/validator.ts:111:5)
          at src/validator.test.ts:65:50
          at Object.<anonymous> (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/toThrowMatchers.js:74:11)
          at Object.throwingMatcher [as toThrow] (../../node_modules/.pnpm/[email protected]/node_modules/expect/build/index.js:320:21)
          at src/validator.test.ts:66:16

      64 |             expect(() => {
      65 |                 validateMimeTypeAndExtensionMatch(filename, 'text/plain')
    > 66 |             }).toThrow(`Invalid filename: path traversal detected in filename "${filename}"`)
         |                ^
      67 |         })
      68 |     })
      69 |

      at src/validator.test.ts:66:16

 PASS  src/handler.test.ts (19.855 s)
  URL Handling For Phoenix Tracer
    √ baseUrl http://localhost:6006 - exporterUrl http://localhost:6006/v1/traces (7 ms)
    √ baseUrl http://localhost:6006/v1/traces - exporterUrl http://localhost:6006/v1/traces (1 ms)
    √ baseUrl https://app.phoenix.arize.com - exporterUrl https://app.phoenix.arize.com/v1/traces
    √ baseUrl https://app.phoenix.arize.com/v1/traces - exporterUrl https://app.phoenix.arize.com/v1/traces (1 ms)
    √ baseUrl https://app.phoenix.arize.com/s/my-space - exporterUrl https://app.phoenix.arize.com/s/my-space/v1/traces (1 ms)
    √ baseUrl https://app.phoenix.arize.com/s/my-space/v1/traces - exporterUrl https://app.phoenix.arize.com/s/my-space/v1/traces (1 ms)
    √ baseUrl https://my-phoenix.com/my-slug - exporterUrl https://my-phoenix.com/my-slug/v1/traces (1 ms)
    √ baseUrl https://my-phoenix.com/my-slug/v1/traces - exporterUrl https://my-phoenix.com/my-slug/v1/traces

Test Suites: 1 failed, 3 passed, 4 total
Tests:       10 failed, 103 passed, 113 total
Snapshots:   0 total
Time:        20.62 s, estimated 57 s
Ran all test suites.
 ELIFECYCLE  Test failed. See above for more details.
 ```

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for catching this. was using github to apply suggested changes from gemini but forgot to update test (should be reflected now)

@jocelynlin-wd
Copy link
Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses a security vulnerability by introducing MIME type and file extension validation for file uploads. The new validation logic in packages/components/src/validator.ts is robust and is accompanied by a comprehensive set of unit tests in validator.test.ts, covering numerous edge cases. The validation has been correctly integrated into all relevant file upload points across the application. Additionally, the new functionality to clean up failed uploads from storage is a great improvement for system stability and resource management. I have one suggestion to improve maintainability by reducing code duplication.

Comment on lines +149 to +153
try {
validateMimeTypeAndExtensionMatch(file.originalname, file.mimetype)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This try...catch block for MIME type validation is repeated across multiple files (openai-assistants/index.ts, documentstore/index.ts, buildChatflow.ts, createAttachment.ts, upsertVector.ts). To improve maintainability and reduce code duplication, consider abstracting this logic into a shared helper function.

For example, you could create a function like this in a utility file:

import { StatusCodes } from 'http-status-codes';
import { validateMimeTypeAndExtensionMatch } from 'flowise-components';
import { InternalFlowiseError } from '../../errors/internalFlowiseError';
import { getErrorMessage } from '../../errors/utils';

export const validateFileOrThrow = (filename: string, mimetype: string): void => {
    try {
        validateMimeTypeAndExtensionMatch(filename, mimetype);
    } catch (error) {
        throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error));
    }
};

Then, you can replace this block with a single call: validateFileOrThrow(file.originalname, file.mimetype);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants