Skip to content

Commit 9c564f2

Browse files
committed
Ensure server side blocks file type mismatches
Signed-off-by: Dave Mackey <dave@davemackey.net>
1 parent 7fcfe49 commit 9c564f2

3 files changed

Lines changed: 84 additions & 2 deletions

File tree

changelogs/fragments/11424.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Add file upload to chat plugin ([#11424](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/11424))

src/plugins/chat/public/components/chat_input.test.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,59 @@ describe('ChatInput', () => {
440440
);
441441
});
442442

443+
it('should reject files with unsupported MIME types', () => {
444+
const onFilesSelected = jest.fn();
445+
const { container } = render(
446+
<ChatInput {...defaultProps} onFilesSelected={onFilesSelected} />
447+
);
448+
449+
const exe = new File(['binary'], 'malware.exe', { type: 'application/x-msdownload' });
450+
selectFiles(container, [exe]);
451+
452+
expect(onFilesSelected).not.toHaveBeenCalled();
453+
expect(mockAddWarning).toHaveBeenCalledWith(expect.stringContaining('malware.exe'));
454+
});
455+
456+
it('should accept files matched by extension when MIME type is empty', () => {
457+
const onFilesSelected = jest.fn();
458+
const { container } = render(
459+
<ChatInput {...defaultProps} onFilesSelected={onFilesSelected} />
460+
);
461+
462+
// Linux browsers often report empty MIME type
463+
const file = new File(['{"key":"val"}'], 'data.json', { type: '' });
464+
selectFiles(container, [file]);
465+
466+
expect(onFilesSelected).toHaveBeenCalledWith([file]);
467+
});
468+
469+
it('should reject files with unsupported extension and empty MIME type', () => {
470+
const onFilesSelected = jest.fn();
471+
const { container } = render(
472+
<ChatInput {...defaultProps} onFilesSelected={onFilesSelected} />
473+
);
474+
475+
const file = new File(['data'], 'image.png', { type: '' });
476+
selectFiles(container, [file]);
477+
478+
expect(onFilesSelected).not.toHaveBeenCalled();
479+
expect(mockAddWarning).toHaveBeenCalledWith(expect.stringContaining('image.png'));
480+
});
481+
482+
it('should keep valid files and reject unsupported files in a mixed selection', () => {
483+
const onFilesSelected = jest.fn();
484+
const { container } = render(
485+
<ChatInput {...defaultProps} onFilesSelected={onFilesSelected} />
486+
);
487+
488+
const good = new File(['hello'], 'notes.txt', { type: 'text/plain' });
489+
const bad = new File(['binary'], 'photo.jpg', { type: 'image/jpeg' });
490+
selectFiles(container, [good, bad]);
491+
492+
expect(onFilesSelected).toHaveBeenCalledWith([good]);
493+
expect(mockAddWarning).toHaveBeenCalledWith(expect.stringContaining('photo.jpg'));
494+
});
495+
443496
it('should truncate file list to remaining capacity', () => {
444497
const onFilesSelected = jest.fn();
445498
const { container } = render(

src/plugins/chat/public/components/chat_input.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/pu
1717
import { CoreStart } from '../../../../core/public';
1818
import {
1919
CHAT_FILE_ACCEPT,
20+
CHAT_ALLOWED_FILE_TYPES,
2021
CHAT_MAX_FILE_ATTACHMENTS as DEFAULT_MAX_FILE_ATTACHMENTS,
2122
ONE_MB,
2223
} from '../../common';
@@ -107,8 +108,34 @@ export const ChatInput: React.FC<ChatInputProps> = ({
107108
return;
108109
}
109110

110-
const valid = files.filter((f) => f.size <= maxFileUploadBytes);
111-
const oversized = files.filter((f) => f.size > maxFileUploadBytes);
111+
const allowedMimeTypes = Object.keys(CHAT_ALLOWED_FILE_TYPES);
112+
const allowedExtensions = Object.values(CHAT_ALLOWED_FILE_TYPES).flat();
113+
const isFileTypeAllowed = (f: File) => {
114+
if (f.type && allowedMimeTypes.includes(f.type)) return true;
115+
const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase();
116+
return allowedExtensions.includes(ext);
117+
};
118+
119+
const typeAllowed = files.filter(isFileTypeAllowed);
120+
const typeRejected = files.filter((f) => !isFileTypeAllowed(f));
121+
122+
if (typeRejected.length > 0) {
123+
const names = typeRejected.map((f) => f.name).join(', ');
124+
notifications.toasts.addWarning(
125+
i18n.translate('chat.input.unsupportedFileType', {
126+
defaultMessage: 'Unsupported file type(s) skipped: {names}. Allowed types: {extensions}.',
127+
values: { names, extensions: allowedExtensions.join(', ') },
128+
})
129+
);
130+
}
131+
132+
if (typeAllowed.length === 0) {
133+
e.target.value = '';
134+
return;
135+
}
136+
137+
const valid = typeAllowed.filter((f) => f.size <= maxFileUploadBytes);
138+
const oversized = typeAllowed.filter((f) => f.size > maxFileUploadBytes);
112139

113140
if (oversized.length > 0) {
114141
const limitMB = (maxFileUploadBytes / ONE_MB).toFixed(1);

0 commit comments

Comments
 (0)