Skip to content

Commit c87202f

Browse files
authored
Sanitize URL from URL.createObjectURL() (#5568)
* Sanitize URL from URL.createObjectURL * Add entry * Add tests * Port to TypeScript * Add comment * Add comment
1 parent 19831c9 commit c87202f

File tree

17 files changed

+249
-42
lines changed

17 files changed

+249
-42
lines changed

.eslintrc.production.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ rules:
3939
message: Use clock functions from ponyfill
4040
- name: setTimeout
4141
message: Use clock functions from ponyfill
42+
no-restricted-properties:
43+
- error
44+
- object: URL
45+
property: createObjectURL
46+
message: Make sure content type is sanitized before presenting the URL in HTML

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
120120
- `@msinternal/botframework-webchat-debug-theme` package for enabling debugging scenarios
121121
- `@msinternal/botframework-webchat-react-hooks` for helpers for React hooks
122122
- Added link sanitization and ESLint rules, in PR [#5564](https://github.com/microsoft/BotFramework-WebChat/pull/5564), by [@compulim](https://github.com/compulim)
123+
- Added blob URL sanitization and ESLint rules, in PR [#5568](https://github.com/microsoft/BotFramework-WebChat/pull/5568), by [@compulim](https://github.com/compulim)
123124

124125
### Changed
125126

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="/test-harness.js"></script>
6+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
7+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
8+
</head>
9+
<body>
10+
<main id="webchat"></main>
11+
<script>
12+
run(async function () {
13+
const {
14+
testHelpers: { createDirectLineEmulator }
15+
} = window;
16+
17+
const { directLine, store } = createDirectLineEmulator();
18+
19+
WebChat.renderWebChat({ directLine, store }, document.getElementById('webchat'));
20+
21+
await pageConditions.uiConnected();
22+
23+
await directLine.emulateIncomingActivity({
24+
attachments: [
25+
{
26+
contentType: 'image/png',
27+
contentUrl:
28+
'data:application/octet-stream;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAK0lEQVQ4T2P8z8Dwn4GKgHHUQIpDczQMKQ5ChtEwHA1DMkJgNNmQEWhoWgBMAiftPRtHngAAAABJRU5ErkJggg=='
29+
}
30+
],
31+
type: 'message'
32+
});
33+
34+
const imageElement = pageElements.activities()[0].querySelector('img');
35+
36+
const imageURL = imageElement.getAttribute('src');
37+
38+
// THEN: This test is only relevant it is a blob URL.
39+
expect(imageURL.startsWith('blob:')).toEqual(true);
40+
41+
const res = await fetch(imageURL);
42+
43+
const contentType = res.headers.get('content-type');
44+
45+
// THEN: Content type should be "application/octet-stream".
46+
expect(contentType).toBe('application/octet-stream');
47+
48+
// THEN: Should show the image properly.
49+
await host.snapshot('local');
50+
});
51+
</script>
52+
</body>
53+
</html>
6.95 KB
Loading
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="/test-harness.js"></script>
6+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
7+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
8+
</head>
9+
<body>
10+
<main id="webchat"></main>
11+
<script>
12+
run(async function () {
13+
const {
14+
testHelpers: { createDirectLineEmulator }
15+
} = window;
16+
17+
const { directLine, store } = createDirectLineEmulator();
18+
19+
WebChat.renderWebChat({ directLine, store }, document.getElementById('webchat'));
20+
21+
await pageConditions.uiConnected();
22+
23+
await directLine.emulateIncomingActivity({
24+
attachments: [
25+
{
26+
contentType: 'image/png',
27+
contentUrl:
28+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAK0lEQVQ4T2P8z8Dwn4GKgHHUQIpDczQMKQ5ChtEwHA1DMkJgNNmQEWhoWgBMAiftPRtHngAAAABJRU5ErkJggg=='
29+
}
30+
],
31+
type: 'message'
32+
});
33+
34+
const imageElement = pageElements.activities()[0].querySelector('img');
35+
36+
const imageURL = imageElement.getAttribute('src');
37+
38+
// THEN: This test is only relevant it is a blob URL.
39+
expect(imageURL.startsWith('blob:')).toEqual(true);
40+
41+
const res = await fetch(imageURL);
42+
43+
const contentType = res.headers.get('content-type');
44+
45+
// THEN: Content type should be "image/png".
46+
expect(contentType).toBe('image/png');
47+
48+
// THEN: Should show the image properly.
49+
await host.snapshot('local');
50+
});
51+
</script>
52+
</body>
53+
</html>
6.95 KB
Loading
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="/test-harness.js"></script>
6+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
7+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
8+
</head>
9+
<body>
10+
<main id="webchat"></main>
11+
<script>
12+
run(async function () {
13+
const {
14+
testHelpers: { createDirectLineEmulator }
15+
} = window;
16+
17+
const { directLine, store } = createDirectLineEmulator();
18+
19+
WebChat.renderWebChat({ directLine, store }, document.getElementById('webchat'));
20+
21+
await pageConditions.uiConnected();
22+
23+
await directLine.emulateIncomingActivity({
24+
attachments: [
25+
{
26+
contentType: 'image/png',
27+
contentUrl:
28+
'data:text/plain;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAK0lEQVQ4T2P8z8Dwn4GKgHHUQIpDczQMKQ5ChtEwHA1DMkJgNNmQEWhoWgBMAiftPRtHngAAAABJRU5ErkJggg=='
29+
}
30+
],
31+
type: 'message'
32+
});
33+
34+
const imageElement = pageElements.activities()[0].querySelector('img');
35+
36+
const imageURL = imageElement.getAttribute('src');
37+
38+
// THEN: This test is only relevant it is a blob URL.
39+
expect(imageURL.startsWith('blob:')).toEqual(true);
40+
41+
const res = await fetch(imageURL);
42+
43+
const contentType = res.headers.get('content-type');
44+
45+
// THEN: Content type should be rectified to "application/octet-stream".
46+
expect(contentType).toBe('application/octet-stream');
47+
48+
// THEN: Should show the image properly.
49+
await host.snapshot('local');
50+
});
51+
</script>
52+
</body>
53+
</html>
6.95 KB
Loading

packages/component/src/Attachment/ImageAttachment.js

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
2+
import React, { memo } from 'react';
3+
import { custom, object, optional, pipe, readonly, safeParse, string, union, type InferInput } from 'valibot';
4+
5+
import readDataURIToBlob from '../Utils/readDataURIToBlob';
6+
import ImageContent from './ImageContent';
7+
import { type WebChatAttachment } from './private/types/WebChatAttachment';
8+
9+
const imageAttachmentPropsSchema = pipe(
10+
object({
11+
attachment: custom<WebChatAttachment>(
12+
value =>
13+
safeParse(
14+
union([
15+
object({
16+
contentUrl: string(),
17+
name: optional(string()),
18+
thumbnailUrl: optional(string())
19+
}),
20+
object({
21+
contentUrl: optional(string()),
22+
name: optional(string()),
23+
thumbnailUrl: string()
24+
})
25+
]),
26+
value
27+
).success
28+
)
29+
}),
30+
readonly()
31+
);
32+
33+
type ImageAttachmentProps = InferInput<typeof imageAttachmentPropsSchema>;
34+
35+
// React component is better with standard function than arrow function.
36+
// eslint-disable-next-line prefer-arrow-callback
37+
const ImageAttachment = memo(function ImageAttachment(props: ImageAttachmentProps) {
38+
const { attachment } = validateProps(imageAttachmentPropsSchema, props);
39+
40+
let imageURL = attachment.thumbnailUrl || attachment.contentUrl;
41+
42+
// To support Content Security Policy, data URI cannot be used.
43+
// We need to parse the data URI into a blob: URL.
44+
const blob = readDataURIToBlob(imageURL);
45+
46+
if (blob) {
47+
// Only allow image/* for image, otherwise, treat it as binary.
48+
// eslint-disable-next-line no-restricted-properties
49+
imageURL = URL.createObjectURL(
50+
new Blob([blob], {
51+
type: blob.type.startsWith('image/') ? blob.type : 'application/octet-stream'
52+
})
53+
);
54+
}
55+
56+
return <ImageContent alt={attachment.name} src={imageURL} />;
57+
});
58+
59+
export default ImageAttachment;

0 commit comments

Comments
 (0)