Skip to content
Merged
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ should change the heading of the (upcoming) version to include a major version b
## @rjsf/core

- Added support for `anyOf`/`oneOf` in `uiSchema`s in the `MultiSchemaField`, fixing [#4039](https://github.com/rjsf-team/react-jsonschema-form/issues/4039)
- Fix potential XSS vulnerability in the preview button of FileWidget, fixing [#4057](https://github.com/rjsf-team/react-jsonschema-form/issues/4057)

## @rjsf/utils

- [#4024](https://github.com/rjsf-team/react-jsonschema-form/issues/4024) Added `base64` to support `encoding` and `decoding` using the `UTF-8` charset to support the characters out of the `Latin1` range.
- Updated `enumOptionsValueForIndex()` to fix issue that filtered enum options with a value that was 0, fixing [#4067](https://github.com/rjsf-team/react-jsonschema-form/issues/4067)
- Changes the way of parsing the data URL, to fix [#4057](https://github.com/rjsf-team/react-jsonschema-form/issues/4057)

## Dev / docs / playground

Expand Down
36 changes: 25 additions & 11 deletions packages/core/src/components/widgets/FileWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,15 @@ function FileInfoPreview<T = any, S extends StrictRJSFSchema = RJSFSchema, F ext
return null;
}

if (type.indexOf('image') !== -1) {
// If type is JPEG or PNG then show image preview.
// Originally, any type of image was supported, but this was changed into a whitelist
// since SVGs and animated GIFs are also images, which are generally considered a security risk.
if (['image/jpeg', 'image/png'].includes(type)) {
return <img src={dataURL} style={{ maxWidth: '100%' }} className='file-preview' />;
}

// otherwise, let users download file

return (
<>
{' '}
Expand Down Expand Up @@ -121,17 +126,26 @@ function FilesInfo<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends F
}

function extractFileInfo(dataURLs: string[]): FileInfoType[] {
return dataURLs
.filter((dataURL) => dataURL)
.map((dataURL) => {
return dataURLs.reduce((acc, dataURL) => {
if (!dataURL) {
return acc;
}
try {
const { blob, name } = dataURItoBlob(dataURL);
return {
dataURL,
name: name,
size: blob.size,
type: blob.type,
};
});
return [
...acc,
{
dataURL,
name: name,
size: blob.size,
type: blob.type,
},
];
} catch (e) {
// Invalid dataURI, so just ignore it.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we want to report the exception to the user? Or make the file upload report it so that they don't wonder why their file disappeared

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibility of Exception: When the ordinary user uploads files in ordinary way, the exception should never happen, since the data URL is always returned, and Base64 encoding/decoding always be successful. Therefore the data should not disappear in an unintuitive way.

Error notification: If the caller injects the malformed string into formData, validator rejects it with .files.0 must match format "data-url", but it would be an issue on the side of the caller.

Although I could also change it by adding error flag in FileInfoType, I decided not to notify the explicit error for this reason.

return acc;
}
}, [] as FileInfoType[]);
}

/**
Expand Down
49 changes: 26 additions & 23 deletions packages/utils/src/dataURItoBlob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,42 @@
* @param dataURI - The `DataUrl` potentially containing name and raw data to be converted to a Blob
* @returns - an object containing a Blob and its name, extracted from the URI
*/
export default function dataURItoBlob(dataURI: string) {
// Split metadata from data
const splitted: string[] = dataURI.split(',');
// Split params
const params: string[] = splitted[0].split(';');
// Get mime-type from params
const type: string = params[0].replace('data:', '');
// Filter the name property from params
const properties = params.filter((param) => {
return param.split('=')[0] === 'name';
});
// Look for the name and use unknown if no name property.
let name: string;
if (properties.length !== 1) {
name = 'unknown';
} else {
// Because we filtered out the other property,
// we only have the name case here, which we decode to make it human-readable
name = decodeURI(properties[0].split('=')[1]);
export default function dataURItoBlob(dataURILike: string) {
// check if is dataURI
if (dataURILike.indexOf('data:') === -1) {
throw new Error('File is invalid: URI must be a dataURI');
}
const dataURI = dataURILike.slice(5);
// split the dataURI into media and base64, with the base64 signature
const splitted = dataURI.split(';base64,');
// if the base64 signature is not present, the latter part will become empty
if (splitted.length !== 2) {
throw new Error('File is invalid: dataURI must be base64');
}
// extract the mime type, media parameters including the name, and the base64 string
const [media, base64] = splitted;
const [mime, ...mediaparams] = media.split(';');
const type = mime || '';

// extract the name from the parameters
const name = decodeURI(
// parse the parameters into key-value pairs, find a key, and extract a value
// if no key is found, then the name is unknown
mediaparams.map((param) => param.split('=')).find(([key]) => key === 'name')?.[1] || 'unknown'
);

// Built the Uint8Array Blob parameter from the base64 string.
try {
const binary = atob(splitted[1]);
const array = [];
const binary = atob(base64);
const array = new Array(binary.length);
for (let i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
array[i] = binary.charCodeAt(i);
}
// Create the blob object
const blob = new window.Blob([new Uint8Array(array)], { type });

return { blob, name };
} catch (error) {
return { blob: { size: 0, type: (error as Error).message }, name: dataURI };
throw new Error('File is invalid: ' + (error as Error).message);
}
}
45 changes: 39 additions & 6 deletions packages/utils/test/dataURItoBlob.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
import { dataURItoBlob } from '../src';

describe('dataURItoBlob()', () => {
it('should pass when the data is empty', () => {
const { blob, name } = dataURItoBlob('data:text/plain;base64,');
expect(name).toEqual('unknown');
expect(blob).toHaveProperty('size', 0);
expect(blob).toHaveProperty('type', 'text/plain');
});
it('should pass when the both body and media type are empty', () => {
const { blob, name } = dataURItoBlob('data:;base64,');
expect(name).toEqual('unknown');
expect(blob).toHaveProperty('size', 0);
expect(blob).toHaveProperty('type', '');
});
it('should return the body is empty', () => {
const { blob, name } = dataURItoBlob('data:application/json;name=test.png;base64,');
expect(name).toEqual('test.png');
expect(blob).toHaveProperty('size', 0);
expect(blob).toHaveProperty('type', 'application/json');
});
it('should throw when the body is not a Base64 encoded dataURI', () => {
expect(() => dataURItoBlob('data:;Hello%20World')).toThrow(new Error('File is invalid: dataURI must be base64'));
expect(() => dataURItoBlob('data:text/plain;Hello%20World')).toThrow(
new Error('File is invalid: dataURI must be base64')
);
expect(() => dataURItoBlob('data:Hello%20World')).toThrow(new Error('File is invalid: dataURI must be base64'));
});
it('should throw if the body is not a valid dataURI', () => {
expect(() => dataURItoBlob('Hello%20World')).toThrow(new Error('File is invalid: URI must be a dataURI'));
expect(() => dataURItoBlob('javascript:alert()')).toThrow(new Error('File is invalid: URI must be a dataURI'));
});

it('should throw the body is not valid Base64', () => {
expect(() => dataURItoBlob('data:text/plain;base64,Hello%20World')).toThrow(
new Error('File is invalid: The string to be decoded contains invalid characters.')
);
expect(() => dataURItoBlob('data:text/plain;base64,こんにちわ')).toThrow(
new Error('File is invalid: The string to be decoded contains invalid characters.')
);
});

it('should return the name of the file if present', () => {
const { blob, name } = dataURItoBlob('data:image/png;name=test.png;base64,VGVzdC5wbmc=');
expect(name).toEqual('test.png');
Expand All @@ -25,10 +64,4 @@ describe('dataURItoBlob()', () => {
expect(blob).toHaveProperty('size', 8);
expect(blob).toHaveProperty('type', 'image/png');
});
it('should return error blob when blob conversion fails', () => {
const { blob, name } = dataURItoBlob('test.png');
expect(name).toEqual('test.png');
expect(blob).toHaveProperty('size', 0);
expect(blob).toHaveProperty('type', 'The string to be decoded contains invalid characters.');
});
});