Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/components/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
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

transform: {
'^.+\\.tsx?$': 'ts-jest'
},
Expand Down
5 changes: 2 additions & 3 deletions packages/components/src/handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { OTLPTraceExporter as ProtoOTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { getPhoenixTracer } from './handler'

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 }.

return { args }
})
}
})

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


describe('URL Handling For Phoenix Tracer', () => {
const apiKey = 'test-api-key'
const projectName = 'test-project-name'
Expand Down
114 changes: 114 additions & 0 deletions packages/components/src/validator.test.ts
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)

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { validateMimeTypeAndExtensionMatch } from './validator'

describe('validateMimeTypeAndExtensionMatch', () => {
describe('valid cases', () => {
it.each([
['document.txt', 'text/plain'],
['page.html', 'text/html'],
['data.json', 'application/json'],
['document.pdf', 'application/pdf'],
['script.js', 'text/javascript'],
['script.js', 'application/javascript'],
['readme.md', 'text/markdown'],
['readme.md', 'text/x-markdown'],
['DOCUMENT.TXT', 'text/plain'],
['Document.TxT', 'text/plain'],
['my.document.txt', 'text/plain']
])('should pass validation for matching MIME type and extension - %s with %s', (filename, mimetype) => {
expect(() => {
validateMimeTypeAndExtensionMatch(filename, mimetype)
}).not.toThrow()
})
})

describe('invalid filename', () => {
it.each([
['empty filename', ''],
['null filename', null],
['undefined filename', undefined],
['non-string filename (number)', 123],
['object filename', {}]
])('should throw error for %s', (_description, filename) => {
expect(() => {
validateMimeTypeAndExtensionMatch(filename as any, 'text/plain')
}).toThrow('Invalid filename: filename is required and must be a string')
})
})

describe('invalid MIME type', () => {
it.each([
['empty MIME type', ''],
['null MIME type', null],
['undefined MIME type', undefined],
['non-string MIME type (number)', 123]
])('should throw error for %s', (_description, mimetype) => {
expect(() => {
validateMimeTypeAndExtensionMatch('file.txt', mimetype as any)
}).toThrow('Invalid MIME type: MIME type is required and must be a string')
})
})

describe('path traversal detection', () => {
it.each([
['filename with ..', '../file.txt'],
['filename with .. in middle', 'path/../file.txt'],
['filename with multle levels of ..', '../../../etc/passwd.txt'],
['filename with ..\\..\\..', '..\\..\\..\\windows\\system32\\file.txt'],
['filename with ....//....//', '....//....//etc/passwd.txt'],
['filename starting with /', '/etc/passwd.txt'],
['Windows absolute path', 'C:\\file.txt'],
['URL encoded path traversal', '%2e%2e/file.txt'],
['URL encoded path traversal multiple levels', '%2e%2e%2f%2e%2e%2f%2e%2e%2ffile.txt'],
['null byte', 'file\0.txt']
])('should throw error for %s', (_description, filename) => {
expect(() => {
validateMimeTypeAndExtensionMatch(filename, 'text/plain')
}).toThrow(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
})
})

describe('files without extensions', () => {
it.each([
['filename without extension', 'file'],
['filename ending with dot', 'file.']
])('should throw error for %s', (_description, filename) => {
expect(() => {
validateMimeTypeAndExtensionMatch(filename, 'text/plain')
}).toThrow('File type not allowed: files must have a valid file extension')
})
})

describe('unsupported MIME types', () => {
it.each([
['application/octet-stream', 'file.txt'],
['invalid-mime-type', 'file.txt'],
['application/x-msdownload', 'malware.exe'],
['application/x-executable', 'script.exe'],
['application/x-msdownload', 'program.EXE'],
['application/octet-stream', 'script.js']
])('should throw error for unsupported MIME type %s with %s', (mimetype, filename) => {
expect(() => {
validateMimeTypeAndExtensionMatch(filename, mimetype)
}).toThrow(`MIME type "${mimetype}" is not supported or does not have a valid file extension mapping`)
})
})

describe('MIME type and extension mismatches', () => {
it.each([
// [filename, mimetype, actualExt, expectedExt]
['file.txt', 'application/json', 'txt', 'json'],
['script.js', 'application/pdf', 'js', 'pdf'],
['page.html', 'text/plain', 'html', 'txt'],
['document.pdf', 'application/json', 'pdf', 'json'],
['data.json', 'text/plain', 'json', 'txt'],
['malware.exe', 'text/plain', 'exe', 'txt'],
['script.js', 'application/json', 'js', 'json']
])('should throw error when extension does not match MIME type - %s with %s', (filename, mimetype, actualExt, expectedExt) => {
expect(() => {
validateMimeTypeAndExtensionMatch(filename, mimetype)
}).toThrow(
`MIME type mismatch: file extension "${actualExt}" does not match declared MIME type "${mimetype}". Expected: ${expectedExt}`
)
})
})
})
67 changes: 67 additions & 0 deletions packages/components/src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { mapMimeTypeToExt } from './utils'

/**
* Validates if a string is a valid UUID v4
* @param {string} uuid The string to validate
Expand Down Expand Up @@ -69,3 +71,68 @@ export const isUnsafeFilePath = (filePath: string): boolean => {

return dangerousPatterns.some((pattern) => pattern.test(filePath))
}

/**
* Validates filename format and security
* @param {string} filename The filename to validate
* @returns {void} Throws an error if validation fails
*/
const validateFilename = (filename: string): void => {
if (!filename || typeof filename !== 'string') {
throw new Error('Invalid filename: filename is required and must be a string')
}
if (isUnsafeFilePath(filename)) {
throw new Error(`Invalid filename: unsafe characters or path traversal attempt detected in filename "${filename}"`)
}
}

/**
* Extracts and normalizes file extension from filename
* @param {string} filename The filename
* @returns {string} The normalized extension (lowercase, without dot) or empty string
*/
const extractFileExtension = (filename: string): string => {
const filenameParts = filename.split('.')
return filenameParts.length > 1 ? filenameParts.pop()!.toLowerCase() : ''
}

/**
* Validates that file extension matches the declared MIME type
*
* This function addresses CVE-2025-61687 by preventing MIME type spoofing attacks.
* It ensures that the file extension matches the declared MIME type, preventing
* attackers from uploading malicious files (e.g., .js file with text/plain MIME type).
*
* @param {string} filename The original filename
* @param {string} mimetype The declared MIME type
* @returns {void} Throws an error if validation fails
*/
export const validateMimeTypeAndExtensionMatch = (filename: string, mimetype: string): void => {
validateFilename(filename)

if (!mimetype || typeof mimetype !== 'string') {
throw new Error('Invalid MIME type: MIME type is required and must be a string')
}

const normalizedExt = extractFileExtension(filename)

if (!normalizedExt) {
// Files without extensions are rejected for security
throw new Error('File type not allowed: files must have a valid file extension')
}

// Get the expected extension from mapMimeTypeToExt (returns extension without dot)
const expectedExt = mapMimeTypeToExt(mimetype)

if (!expectedExt) {
// If mapMimeTypeToExt doesn't recognize the MIME type, it's not supported
throw new Error(`MIME type "${mimetype}" is not supported or does not have a valid file extension mapping`)
}

// Ensure the file extension matches the expected extension for the MIME type
if (normalizedExt !== expectedExt) {
throw new Error(
`MIME type mismatch: file extension "${normalizedExt}" does not match declared MIME type "${mimetype}". Expected: ${expectedExt}`
)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Request, Response, NextFunction } from 'express'
import { StatusCodes } from 'http-status-codes'
import { validateMimeTypeAndExtensionMatch } from 'flowise-components'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import openAIAssistantVectorStoreService from '../../services/openai-assistants-vector-store'
import { getErrorMessage } from '../../errors/utils'

const getAssistantVectorStore = async (req: Request, res: Response, next: NextFunction) => {
try {
Expand Down Expand Up @@ -142,6 +144,14 @@ const uploadFilesToAssistantVectorStore = async (req: Request, res: Response, ne
for (const file of files) {
// Address file name with special characters: https://github.com/expressjs/multer/issues/1104
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8')

// Validate file extension, MIME type, and content to prevent security vulnerabilities
try {
validateMimeTypeAndExtensionMatch(file.originalname, file.mimetype)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error))
}
Comment on lines +149 to +153
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);


uploadFiles.push({
filePath: file.path ?? file.key,
fileName: file.originalname
Expand Down
11 changes: 10 additions & 1 deletion packages/server/src/controllers/openai-assistants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import openaiAssistantsService from '../../services/openai-assistants'
import contentDisposition from 'content-disposition'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
import { streamStorageFile } from 'flowise-components'
import { streamStorageFile, validateMimeTypeAndExtensionMatch } from 'flowise-components'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { ChatFlow } from '../../database/entities/ChatFlow'
import { Workspace } from '../../enterprise/database/entities/workspace.entity'
import { getErrorMessage } from '../../errors/utils'

// List available assistants
const getAllOpenaiAssistants = async (req: Request, res: Response, next: NextFunction) => {
Expand Down Expand Up @@ -104,6 +105,14 @@ const uploadAssistantFiles = async (req: Request, res: Response, next: NextFunct
for (const file of files) {
// Address file name with special characters: https://github.com/expressjs/multer/issues/1104
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8')

// Validate file extension, MIME type, and content to prevent security vulnerabilities
try {
validateMimeTypeAndExtensionMatch(file.originalname, file.mimetype)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error))
}

uploadFiles.push({
filePath: file.path ?? file.key,
fileName: file.originalname
Expand Down
10 changes: 9 additions & 1 deletion packages/server/src/services/documentstore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
mapMimeTypeToInputField,
removeFilesFromStorage,
removeSpecificFileFromStorage,
removeSpecificFileFromUpload
removeSpecificFileFromUpload,
validateMimeTypeAndExtensionMatch
} from 'flowise-components'
import { StatusCodes } from 'http-status-codes'
import { cloneDeep, omit } from 'lodash'
Expand Down Expand Up @@ -1827,6 +1828,13 @@ const upsertDocStore = async (
// Address file name with special characters: https://github.com/expressjs/multer/issues/1104
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8')

// Validate file extension, MIME type, and content to prevent security vulnerabilities
try {
validateMimeTypeAndExtensionMatch(file.originalname, file.mimetype)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error))
}

try {
checkStorage(orgId, subscriptionId, usageCacheManager)
const { totalSize } = await addArrayFilesToStorage(
Expand Down
19 changes: 18 additions & 1 deletion packages/server/src/utils/buildChatflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
removeSpecificFileFromUpload,
EvaluationRunner,
handleEscapeCharacters,
IServerSideEventStreamer
IServerSideEventStreamer,
validateMimeTypeAndExtensionMatch
} from 'flowise-components'
import { StatusCodes } from 'http-status-codes'
import {
Expand Down Expand Up @@ -354,6 +355,14 @@ export const executeFlow = async ({
const splitDataURI = upload.data.split(',')
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
const mime = splitDataURI[0].split(':')[1].split(';')[0]

// Validate file extension, MIME type, and content to prevent security vulnerabilities
try {
validateMimeTypeAndExtensionMatch(filename, mime)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error))
}

const { totalSize } = await addSingleFileToStorage(mime, bf, filename, orgId, chatflowid, chatId)
await updateStorageUsage(orgId, workspaceId, totalSize, usageCacheManager)
upload.type = 'stored-file'
Expand Down Expand Up @@ -418,6 +427,14 @@ export const executeFlow = async ({
const fileBuffer = await getFileFromUpload(file.path ?? file.key)
// Address file name with special characters: https://github.com/expressjs/multer/issues/1104
file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8')

// Validate file extension, MIME type, and content to prevent security vulnerabilities
try {
validateMimeTypeAndExtensionMatch(file.originalname, file.mimetype)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error))
}

const { path: storagePath, totalSize } = await addArrayFilesToStorage(
file.mimetype,
fileBuffer,
Expand Down
33 changes: 32 additions & 1 deletion packages/server/src/utils/createAttachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
mapExtToInputField,
mapMimeTypeToInputField,
removeSpecificFileFromUpload,
removeSpecificFileFromStorage,
isValidUUID,
isPathTraversal
isPathTraversal,
validateMimeTypeAndExtensionMatch
} from 'flowise-components'
import { getRunningExpressApp } from './getRunningExpressApp'
import { getErrorMessage } from '../errors/utils'
Expand Down Expand Up @@ -141,6 +143,15 @@ export const createFileAttachment = async (req: Request) => {
)
}

// Security fix: Verify file extension matches the declared MIME type
// This prevents MIME type spoofing attacks (e.g., uploading .js file with text/plain MIME type)
// This addresses the vulnerability (CVE-2025-61687)
try {
validateMimeTypeAndExtensionMatch(file.originalname, file.mimetype)
} catch (error) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, getErrorMessage(error))
}

await checkStorage(orgId, subscriptionId, appServer.usageCacheManager)

const fileBuffer = await getFileFromUpload(file.path ?? file.key)
Expand Down Expand Up @@ -174,6 +185,9 @@ export const createFileAttachment = async (req: Request) => {

await removeSpecificFileFromUpload(file.path ?? file.key)

// Track sanitized filename for cleanup if processing fails
const sanitizedFilename = fileNames.length > 0 ? fileNames[0] : undefined

try {
const nodeData = {
inputs: {
Expand Down Expand Up @@ -204,6 +218,23 @@ export const createFileAttachment = async (req: Request) => {
content
})
} catch (error) {
// Security: Clean up storage if processing failed, which includes invalid file type or content detacted from loader
if (sanitizedFilename) {
console.info(`Clean up storage for ${file.originalname} (${sanitizedFilename}). Reason: ${getErrorMessage(error)}`)
try {
const { totalSize: newTotalSize } = await removeSpecificFileFromStorage(
orgId,
chatflowid,
chatId,
sanitizedFilename
)
await updateStorageUsage(orgId, workspaceId, newTotalSize, appServer.usageCacheManager)
} catch (cleanupError) {
console.error(
`Failed to cleanup storage for ${file.originalname} (${sanitizedFilename}) - ${getErrorMessage(cleanupError)}`
)
}
}
throw new Error(`Failed createFileAttachment: ${file.originalname} (${file.mimetype} - ${getErrorMessage(error)}`)
}
}
Expand Down
Loading