Skip to content

Commit da14239

Browse files
authored
fix(chat): Fix synthetic message ID mismatch and XSS in export (#11793)
- Fix synthetic message ID mismatch: when assistantMsg.content is an array, pass the original assistantMsg via shareTargetMessage prop through MessageRow to ShareModal so the timeline ID lookup succeeds - Add guard in collectChatExportData to throw clear error if target message not found in timeline (caught by ShareModal error handler) - Escape questionImage.mimeType and base64 in PDF template img tag to prevent XSS via crafted mimeType attribute breakout - Add XSS test for questionImage mimeType escaping Signed-off-by: Justin Kim <jungkm@amazon.com>
1 parent 59e50af commit da14239

5 files changed

Lines changed: 22 additions & 2 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ const ChatMessagesComponent: React.FC<ChatMessagesProps> = ({
465465
}}
466466
timeline={isShareable ? timeline : undefined}
467467
threadId={isShareable ? threadId : undefined}
468+
shareTargetMessage={isShareable ? assistantMsg : undefined}
468469
/>
469470
));
470471
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ interface MessageRowProps {
1818
onResend?: (message: Message) => void;
1919
timeline?: Message[];
2020
threadId?: string;
21+
/** Original assistant message for ShareModal when rendering synthetic array content messages */
22+
shareTargetMessage?: AssistantMessage;
2123
}
2224

2325
export const MessageRow: React.FC<MessageRowProps> = ({
@@ -26,6 +28,7 @@ export const MessageRow: React.FC<MessageRowProps> = ({
2628
onResend,
2729
timeline,
2830
threadId,
31+
shareTargetMessage,
2932
}) => {
3033
const [isHovered, setIsHovered] = useState(false);
3134
const [showShareModal, setShowShareModal] = useState(false);
@@ -157,7 +160,7 @@ export const MessageRow: React.FC<MessageRowProps> = ({
157160
<ShareModal
158161
onClose={() => setShowShareModal(false)}
159162
timeline={timeline}
160-
targetMessage={message as AssistantMessage}
163+
targetMessage={(shareTargetMessage || message) as AssistantMessage}
161164
threadId={threadId}
162165
/>
163166
)}

src/plugins/chat/public/services/export/investigation_export_service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export async function collectChatExportData(
2727
options: ChatExportOptions
2828
): Promise<ChatExportData> {
2929
const targetIndex = timeline.findIndex((m) => m.id === targetMessage.id);
30+
if (targetIndex === -1) {
31+
throw new Error(`Target message ${targetMessage.id} not found in timeline`);
32+
}
3033
const { text: question, image: questionImage } = findPrecedingQuestion(timeline, targetIndex);
3134

3235
return {

src/plugins/chat/public/services/export/pdf_template.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,15 @@ describe('generatePDFReport', () => {
170170
expect(result).not.toContain('<script>alert');
171171
expect(result).toContain('&lt;script&gt;');
172172
});
173+
174+
it('should escape HTML in questionImage mimeType', () => {
175+
const data = {
176+
...baseData,
177+
questionImage: { base64: 'abc', mimeType: '" onerror="alert(1)' },
178+
};
179+
const result = generatePDFReport(data, baseOptions);
180+
// The " should be escaped to &quot; preventing attribute breakout
181+
expect(result).not.toContain('" onerror="');
182+
expect(result).toContain('&quot;');
183+
});
173184
});

src/plugins/chat/public/services/export/pdf_template.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ export function generatePDFReport(data: ChatExportData, options: ChatExportOptio
134134
<div class="text">${escape(data.question)}</div>
135135
${
136136
data.questionImage
137-
? `<img src="data:${data.questionImage.mimeType};base64,${data.questionImage.base64}" alt="Question context" style="max-width:100%;border-radius:6px;margin-top:12px;" />`
137+
? `<img src="data:${escape(data.questionImage.mimeType)};base64,${escape(
138+
data.questionImage.base64
139+
)}" alt="Question context" style="max-width:100%;border-radius:6px;margin-top:12px;" />`
138140
: ''
139141
}
140142
</div>

0 commit comments

Comments
 (0)