Improper Neutralization of HTML Tags in a Web Page in libredesk
Description
Libredesk is a self-hosted customer support desk. Prior to version 0.8.6-beta, LibreDesk is vulnerable to stored HTML injection in the contact notes feature. When adding notes via POST /api/v1/contacts/{id}/notes, the backend automatically wraps user input in <p> tags. However, by intercepting the request and removing the <p> tag, an attacker can inject arbitrary HTML elements such as forms and images, which are then stored and rendered without proper sanitization. This can lead to phishing, CSRF-style forced actions, and UI redress attacks. This issue has been patched in version 0.8.6-beta.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/abhinavxd/libredeskGo | < 0.8.6-beta | 0.8.6-beta |
Affected products
1Patches
1270347849943fix: use vue-letter to render html in contact notes, command box. This prevents forms from being injected in contact notes, macros etc.
9 files changed · +234 −352
frontend/package.json+0 −1 modified@@ -53,7 +53,6 @@ "tailwind-merge": "^2.3.0", "vee-validate": "^4.15.0", "vue": "^3.4.37", - "vue-dompurify-html": "^5.2.0", "vue-i18n": "9", "vue-letter": "^0.2.0", "vue-picture-cropper": "^0.7.0",
frontend/pnpm-lock.yaml+0 −26 modified@@ -113,9 +113,6 @@ importers: vue: specifier: ^3.4.37 version: 3.5.13(typescript@5.7.3) - vue-dompurify-html: - specifier: ^5.2.0 - version: 5.2.0(vue@3.5.13(typescript@5.7.3)) vue-i18n: specifier: '9' version: 9.14.5(vue@3.5.13(typescript@5.7.3)) @@ -1209,9 +1206,6 @@ packages: '@types/topojson@3.2.6': resolution: {integrity: sha512-ppfdlxjxofWJ66XdLgIlER/85RvpGyfOf8jrWf+3kVIjEatFxEZYD/Ea83jO672Xu1HRzd/ghwlbcZIUNHTskw==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/web-bluetooth@0.0.20': resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} @@ -1912,9 +1906,6 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dompurify@3.2.4: - resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3447,11 +3438,6 @@ packages: '@vue/composition-api': optional: true - vue-dompurify-html@5.2.0: - resolution: {integrity: sha512-GX+BStkKEJ8wu/+hU1EK2nu/gzXWhb4XzBu6aowpsuU/3nkvXvZ2jx4nZ9M3jtS/Vu7J7MtFXjc7x3cWQ+zbVQ==} - peerDependencies: - vue: ^3.0.0 - vue-eslint-parser@9.4.3: resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -4611,9 +4597,6 @@ snapshots: '@types/topojson-simplify': 3.0.3 '@types/topojson-specification': 1.0.5 - '@types/trusted-types@2.0.7': - optional: true - '@types/web-bluetooth@0.0.20': {} '@types/web-bluetooth@0.0.21': {} @@ -5449,10 +5432,6 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.2.4: - optionalDependencies: - '@types/trusted-types': 2.0.7 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7086,11 +7065,6 @@ snapshots: dependencies: vue: 3.5.13(typescript@5.7.3) - vue-dompurify-html@5.2.0(vue@3.5.13(typescript@5.7.3)): - dependencies: - dompurify: 3.2.4 - vue: 3.5.13(typescript@5.7.3) - vue-eslint-parser@9.4.3(eslint@8.57.1): dependencies: debug: 4.4.0(supports-color@8.1.1)
frontend/src/features/command/CommandBox.vue+4 −2 modified@@ -71,9 +71,10 @@ <p class="text-xs font-semibold text-foreground"> {{ $t('command.replyPreview') }} </p> - <div + <Letter + :html="replyContent" + :allowedSchemas="['cid', 'https', 'http', 'mailto']" class="w-full min-h-200 p-2 bg-muted/50 rounded overflow-auto shadow native-html" - v-dompurify-html="replyContent" /> </div> @@ -237,6 +238,7 @@ import { Calendar } from '@/components/ui/calendar' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useI18n } from 'vue-i18n' +import { Letter } from 'vue-letter' const conversationStore = useConversationStore() const macroStore = useMacroStore()
frontend/src/features/contact/ContactNotes.vue+6 −1 modified@@ -99,7 +99,11 @@ <!-- Note content --> <CardContent class="pt-4 pb-5"> - <p class="whitespace-pre-wrap text-sm native-html" v-dompurify-html="note.note"></p> + <Letter + :html="note.note" + :allowedSchemas="['cid', 'https', 'http', 'mailto']" + class="whitespace-pre-wrap text-sm native-html" + /> </CardContent> </Card> </div> @@ -155,6 +159,7 @@ import { EMITTER_EVENTS } from '@/constants/emitterEvents.js' import { handleHTTPError } from '@/utils/http' import { getInitials } from '@/utils/strings' import { useUserStore } from '@/stores/user' +import { Letter } from 'vue-letter' import api from '@/api' const props = defineProps({ contactId: Number })
frontend/src/features/conversation/message/AgentMessageBubble.vue+0 −156 removed@@ -1,156 +0,0 @@ -<template> - <div class="flex flex-col items-end text-left"> - <!-- Sender Name --> - <div class="pr-[47px] mb-1"> - <p class="text-muted-foreground text-sm font-medium"> - {{ getFullName }} - </p> - </div> - - <!-- Message Bubble --> - <div class="flex flex-row gap-2 justify-end"> - <!-- Bubble Wrapper with max 80% width --> - <div class="w-4/5 flex justify-end"> - <div - class="flex flex-col justify-end message-bubble relative" - :class="{ - 'bg-[#FEF1E1] dark:bg-[#4C3A24]': message.private, - 'border border-border': !message.private, - 'opacity-50 animate-pulse': message.status === 'pending', - 'border-red-400': message.status === 'failed' - }" - > - <!-- Message Envelope --> - <MessageEnvelope :message="message" v-if="showEnvelope" /> - - <hr class="mb-2" v-if="showEnvelope" /> - - <!-- Message --> - <div - v-dompurify-html="messageContent" - class="whitespace-pre-wrap break-words overflow-wrap-anywhere native-html" - :class="{ 'mb-3': message.attachments.length > 0 }" - /> - - <!-- Attachments --> - <MessageAttachmentPreview :attachments="nonInlineAttachments" /> - - <!-- Spinner for Pending Messages --> - <Spinner v-if="message.status === 'pending'" size="w-4 h-4" /> - - <!-- Icons --> - <div class="flex items-center space-x-2 mt-2 self-end"> - <Lock :size="10" v-if="isPrivateMessage" class="text-muted-foreground" /> - <Check :size="14" v-if="showCheckCheck" class="text-green-500" /> - <RotateCcw - size="10" - @click="retryMessage(message)" - class="cursor-pointer text-muted-foreground hover:text-foreground transition-colors duration-200" - v-if="showRetry" - /> - </div> - </div> - </div> - - <!-- Avatar --> - <Avatar class="cursor-pointer w-8 h-8"> - <AvatarImage :src="getAvatar" /> - <AvatarFallback class="font-medium"> - {{ avatarFallback }} - </AvatarFallback> - </Avatar> - </div> - - <!-- Timestamp tooltip --> - <div class="pr-[47px]"> - <Tooltip> - <TooltipTrigger> - <span class="text-muted-foreground text-xs mt-1"> - {{ formatMessageTimestamp(message.created_at) }} - </span> - </TooltipTrigger> - <TooltipContent> - {{ formatFullTimestamp(message.created_at) }} - </TooltipContent> - </Tooltip> - </div> - </div> -</template> - -<script setup> -import { computed } from 'vue' -import { useConversationStore } from '@/stores/conversation' -import { Lock, RotateCcw, Check } from 'lucide-vue-next' -import { revertCIDToImageSrc } from '@/utils/strings' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { Spinner } from '@/components/ui/spinner' -import { formatMessageTimestamp, formatFullTimestamp } from '@/utils/datetime' -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' -import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue' -import MessageEnvelope from './MessageEnvelope.vue' -import api from '@/api' - -const props = defineProps({ - message: Object -}) -const convStore = useConversationStore() - -const participant = computed(() => { - return convStore.conversation?.participants?.[props.message.sender_id] ?? {} -}) - -const getFullName = computed(() => { - const firstName = participant.value?.first_name ?? 'User' - const lastName = participant.value?.last_name ?? '' - return `${firstName} ${lastName}` -}) - -const getAvatar = computed(() => { - return participant.value?.avatar_url || '' -}) - -const messageContent = computed(() => { - return revertCIDToImageSrc(props.message.content) -}) - -const nonInlineAttachments = computed(() => - props.message.attachments.filter((attachment) => attachment.disposition !== 'inline') -) - -const isPrivateMessage = computed(() => { - return props.message.private -}) - -const showCheckCheck = computed(() => { - return props.message.status == 'sent' && !isPrivateMessage.value -}) - -const showRetry = computed(() => { - return props.message.status == 'failed' -}) - -const avatarFallback = computed(() => { - const firstName = participant.value?.first_name ?? 'A' - return firstName.toUpperCase().substring(0, 2) -}) - -const retryMessage = (msg) => { - api.retryMessage(convStore.current.uuid, msg.uuid) -} - -const showEnvelope = computed(() => { - return ( - props.message.meta?.from?.length || - props.message.meta?.to?.length || - props.message.meta?.cc?.length || - props.message.meta?.bcc?.length || - props.message.meta?.subject - ) -}) -</script> - -<style scoped> -.overflow-wrap-anywhere { - overflow-wrap: anywhere; -} -</style>
frontend/src/features/conversation/message/ContactMessageBubble.vue+0 −158 removed@@ -1,158 +0,0 @@ -<template> - <div class="flex flex-col items-start"> - <!-- Sender Name --> - <div class="pl-[47px] mb-1"> - <p class="text-muted-foreground text-sm font-medium"> - {{ getFullName }} - </p> - </div> - - <!-- Message Bubble --> - <div class="flex flex-row gap-2 w-full"> - <!-- Avatar --> - <Avatar class="cursor-pointer w-8 h-8"> - <AvatarImage :src="getAvatar" /> - <AvatarFallback class="font-medium"> - {{ avatarFallback }} - </AvatarFallback> - </Avatar> - - <!-- Message Content --> - <div class="w-4/5" :style="'contain: inline-size;'"> - <div - class="flex flex-col justify-end message-bubble" - :class="{ - 'show-quoted-text': showQuotedText, - 'hide-quoted-text': !showQuotedText - }" - > - <MessageEnvelope :message="message" v-if="showEnvelope" /> - - <hr class="mb-2" v-if="showEnvelope" /> - - <!-- Message Text --> - <div - v-if="message.content_type === 'text'" - class="mb-1 native-html whitespace-pre-wrap" - :class="{ 'mb-3': message.attachments.length > 0 }" - > - {{ sanitizedMessageContent }} - </div> - <Letter - v-else - :html="sanitizedMessageContent" - :allowedSchemas="['cid', 'https', 'http', 'mailto']" - class="mb-1 native-html" - :class="{ 'mb-3': message.attachments.length > 0 }" - /> - - <!-- Quoted Text Toggle --> - <div - v-if="hasQuotedContent" - @click="toggleQuote" - class="text-xs cursor-pointer text-muted-foreground px-2 py-1 w-max hover:bg-muted hover:text-primary rounded transition-all" - > - {{ - showQuotedText ? t('conversation.hideQuotedText') : t('conversation.showQuotedText') - }} - </div> - - <!-- Attachments --> - <MessageAttachmentPreview :attachments="nonInlineAttachments" /> - </div> - </div> - </div> - - <!-- Timestamp --> - <div class="pl-[47px]"> - <Tooltip> - <TooltipTrigger> - <span class="text-muted-foreground text-xs mt-1"> - {{ formatMessageTimestamp(message.created_at) }} - </span> - </TooltipTrigger> - <TooltipContent> - <p> - {{ formatFullTimestamp(message.created_at) }} - </p> - </TooltipContent> - </Tooltip> - </div> - </div> -</template> - -<script setup> -import { computed, ref } from 'vue' -import { useConversationStore } from '@/stores/conversation' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' -import { Letter } from 'vue-letter' -import { formatMessageTimestamp, formatFullTimestamp } from '@/utils/datetime' -import { useAppSettingsStore } from '@/stores/appSettings' -import { useI18n } from 'vue-i18n' -import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue' -import MessageEnvelope from './MessageEnvelope.vue' - -const props = defineProps({ - message: Object -}) - -const convStore = useConversationStore() -const settingsStore = useAppSettingsStore() -const showQuotedText = ref(false) -const { t } = useI18n() - -const participant = computed(() => { - return convStore.conversation?.participants?.[props.message.sender_id] ?? {} -}) - -const getAvatar = computed(() => { - return participant.value?.avatar_url || '' -}) -const sanitizedMessageContent = computed(() => { - let content = props.message.content || '' - const baseUrl = settingsStore.settings['app.root_url'] - - // Replace CID with URL for inline attachments from the message. - content = props.message.attachments.reduce( - (acc, { content_id, url }) => acc.replace(new RegExp(`cid:${content_id}`, 'g'), url), - content - ) - - // Add base URL to all img src starting with /uploads/ as vue-letter does not allow relative URLs. - content = content.replace(/src="\/uploads\//g, `src="${baseUrl}/uploads/`) - - return content -}) - -const hasQuotedContent = computed(() => sanitizedMessageContent.value.includes('<blockquote')) - -const toggleQuote = () => { - showQuotedText.value = !showQuotedText.value -} - -const nonInlineAttachments = computed(() => - props.message.attachments.filter((attachment) => attachment.disposition !== 'inline') -) - -const getFullName = computed(() => { - const firstName = participant.value?.first_name ?? 'User' - const lastName = participant.value?.last_name ?? '' - return `${firstName} ${lastName}` -}) - -const avatarFallback = computed(() => { - const firstName = participant.value?.first_name ?? 'U' - return firstName.toUpperCase().substring(0, 2) -}) - -const showEnvelope = computed(() => { - return ( - props.message.meta?.from?.length || - props.message.meta?.to?.length || - props.message.meta?.cc?.length || - props.message.meta?.bcc?.length || - props.message.meta?.subject - ) -}) -</script>
frontend/src/features/conversation/message/MessageBubble.vue+220 −0 added@@ -0,0 +1,220 @@ +<template> + <div class="flex flex-col text-left" :class="isOutgoing ? 'items-end' : 'items-start'"> + <!-- Sender Name --> + <div class="mb-1" :class="isOutgoing ? 'pr-[47px]' : 'pl-[47px]'"> + <p class="text-muted-foreground text-sm font-medium"> + {{ getFullName }} + </p> + </div> + + <!-- Message Bubble --> + <div class="flex flex-row gap-2 w-full" :class="{ 'justify-end': isOutgoing }"> + <!-- Avatar (left for incoming) --> + <Avatar v-if="!isOutgoing" class="cursor-pointer w-8 h-8"> + <AvatarImage :src="getAvatar" /> + <AvatarFallback class="font-medium"> + {{ avatarFallback }} + </AvatarFallback> + </Avatar> + + <!-- Bubble Wrapper with max 80% width --> + <div + class="w-4/5" + :class="{ 'flex justify-end': isOutgoing }" + style="contain: inline-size" + > + <div + class="flex flex-col justify-end message-bubble" + :class="bubbleClasses" + > + <!-- Message Envelope --> + <MessageEnvelope :message="message" v-if="showEnvelope" /> + + <hr class="mb-2" v-if="showEnvelope" /> + + <!-- Message Content --> + <div + v-if="message.content_type === 'text'" + class="mb-1 native-html whitespace-pre-wrap" + :class="{ 'mb-3': message.attachments.length > 0 }" + > + {{ sanitizedContent }} + </div> + <Letter + v-else + :html="sanitizedContent" + :allowedSchemas="['cid', 'https', 'http', 'mailto']" + class="mb-1 native-html whitespace-pre-wrap break-words" + :class="{ 'mb-3': message.attachments.length > 0 }" + /> + + <!-- Quoted Text Toggle (incoming only) --> + <div + v-if="!isOutgoing && hasQuotedContent" + @click="toggleQuote" + class="text-xs cursor-pointer text-muted-foreground px-2 py-1 w-max hover:bg-muted hover:text-primary rounded transition-all" + > + {{ showQuotedText ? t('conversation.hideQuotedText') : t('conversation.showQuotedText') }} + </div> + + <!-- Attachments --> + <MessageAttachmentPreview :attachments="nonInlineAttachments" /> + + <!-- Spinner for Pending Messages (outgoing only) --> + <Spinner v-if="isOutgoing && message.status === 'pending'" size="w-4 h-4" /> + + <!-- Status Icons (outgoing only) --> + <div v-if="isOutgoing" class="flex items-center space-x-2 mt-2 self-end"> + <Lock :size="10" v-if="isPrivateMessage" class="text-muted-foreground" /> + <Check :size="14" v-if="showCheckCheck" class="text-green-500" /> + <RotateCcw + size="10" + @click="retryMessage(message)" + class="cursor-pointer text-muted-foreground hover:text-foreground transition-colors duration-200" + v-if="showRetry" + /> + </div> + </div> + </div> + + <!-- Avatar (right for outgoing) --> + <Avatar v-if="isOutgoing" class="cursor-pointer w-8 h-8"> + <AvatarImage :src="getAvatar" /> + <AvatarFallback class="font-medium"> + {{ avatarFallback }} + </AvatarFallback> + </Avatar> + </div> + + <!-- Timestamp tooltip --> + <div :class="isOutgoing ? 'pr-[47px]' : 'pl-[47px]'"> + <Tooltip> + <TooltipTrigger> + <span class="text-muted-foreground text-xs mt-1"> + {{ formatMessageTimestamp(message.created_at) }} + </span> + </TooltipTrigger> + <TooltipContent> + <p>{{ formatFullTimestamp(message.created_at) }}</p> + </TooltipContent> + </Tooltip> + </div> + </div> +</template> + +<script setup> +import { computed, ref } from 'vue' +import { useConversationStore } from '@/stores/conversation' +import { useAppSettingsStore } from '@/stores/appSettings' +import { useI18n } from 'vue-i18n' +import { Lock, RotateCcw, Check } from 'lucide-vue-next' +import { revertCIDToImageSrc } from '@/utils/strings' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { Spinner } from '@/components/ui/spinner' +import { formatMessageTimestamp, formatFullTimestamp } from '@/utils/datetime' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Letter } from 'vue-letter' +import MessageAttachmentPreview from '@/features/conversation/message/attachment/MessageAttachmentPreview.vue' +import MessageEnvelope from './MessageEnvelope.vue' +import api from '@/api' + +const props = defineProps({ + message: Object, + direction: { + type: String, + validator: (v) => ['incoming', 'outgoing'].includes(v) + } +}) + +const convStore = useConversationStore() +const settingsStore = useAppSettingsStore() +const { t } = useI18n() + +// Direction helpers +const isOutgoing = computed(() => props.direction === 'outgoing') + +// Participant info +const participant = computed(() => { + return convStore.conversation?.participants?.[props.message.sender_id] ?? {} +}) + +const getFullName = computed(() => { + const firstName = participant.value?.first_name ?? 'User' + const lastName = participant.value?.last_name ?? '' + return `${firstName} ${lastName}` +}) + +const getAvatar = computed(() => { + return participant.value?.avatar_url || '' +}) + +const avatarFallback = computed(() => { + const firstName = participant.value?.first_name ?? (isOutgoing.value ? 'A' : 'U') + return firstName.toUpperCase().substring(0, 2) +}) + +// Content sanitization - different processing for incoming vs outgoing +const sanitizedContent = computed(() => { + let content = props.message.content || '' + + if (isOutgoing.value) { + return revertCIDToImageSrc(content) + } else { + const baseUrl = settingsStore.settings['app.root_url'] + content = props.message.attachments.reduce( + (acc, { content_id, url }) => acc.replace(new RegExp(`cid:${content_id}`, 'g'), url), + content + ) + content = content.replace(/src="\/uploads\//g, `src="${baseUrl}/uploads/`) + return content + } +}) + +const nonInlineAttachments = computed(() => + props.message.attachments.filter((attachment) => attachment.disposition !== 'inline') +) + +// Bubble classes - conditional based on direction +const bubbleClasses = computed(() => ({ + // Outgoing-specific: private message styling + 'bg-[#FEF1E1] dark:bg-[#4C3A24]': isOutgoing.value && props.message.private, + 'border border-border': isOutgoing.value && !props.message.private, + 'opacity-50 animate-pulse': isOutgoing.value && props.message.status === 'pending', + 'border-red-400': isOutgoing.value && props.message.status === 'failed', + relative: isOutgoing.value, + // Incoming-specific: quoted text visibility + 'show-quoted-text': !isOutgoing.value && showQuotedText.value, + 'hide-quoted-text': !isOutgoing.value && !showQuotedText.value +})) + +// Outgoing-only computed properties +const isPrivateMessage = computed(() => isOutgoing.value && props.message.private) +const showCheckCheck = computed( + () => isOutgoing.value && props.message.status === 'sent' && !isPrivateMessage.value +) +const showRetry = computed(() => isOutgoing.value && props.message.status === 'failed') + +const retryMessage = (msg) => { + api.retryMessage(convStore.current.uuid, msg.uuid) +} + +// Incoming-only: quoted text toggle +const showQuotedText = ref(false) +const hasQuotedContent = computed( + () => !isOutgoing.value && sanitizedContent.value.includes('<blockquote') +) +const toggleQuote = () => { + showQuotedText.value = !showQuotedText.value +} + +// Envelope visibility (both directions) +const showEnvelope = computed(() => { + return ( + props.message.meta?.from?.length || + props.message.meta?.to?.length || + props.message.meta?.cc?.length || + props.message.meta?.bcc?.length || + props.message.meta?.subject + ) +}) +</script> \ No newline at end of file
frontend/src/features/conversation/message/MessageList.vue+4 −6 modified@@ -31,12 +31,11 @@ 'pt-4': index === 0 }" > - <div v-if="!message.private"> - <ContactMessageBubble :message="message" v-if="message.type === 'incoming'" /> - <AgentMessageBubble :message="message" v-if="message.type === 'outgoing'" /> + <div v-if="!message.private && message.type !== 'activity'"> + <MessageBubble :message="message" :direction="message.type" /> </div> <div v-else-if="isPrivateNote(message)"> - <AgentMessageBubble :message="message" v-if="message.type === 'outgoing'" /> + <MessageBubble :message="message" direction="outgoing" /> </div> <div v-else-if="message.type === 'activity'"> <ActivityMessageBubble :message="message" /> @@ -75,9 +74,8 @@ <script setup> import { ref, onMounted, watch } from 'vue' -import ContactMessageBubble from './ContactMessageBubble.vue' +import MessageBubble from './MessageBubble.vue' import ActivityMessageBubble from './ActivityMessageBubble.vue' -import AgentMessageBubble from './AgentMessageBubble.vue' import { useConversationStore } from '@/stores/conversation' import { useUserStore } from '@/stores/user' import { Button } from '@/components/ui/button'
frontend/src/main.js+0 −2 modified@@ -7,7 +7,6 @@ import mitt from 'mitt' import api from './api' import './assets/styles/main.scss' import './utils/strings.js' -import VueDOMPurifyHTML from 'vue-dompurify-html' import Root from './Root.vue' const setFavicon = (url) => { @@ -59,7 +58,6 @@ async function initApp () { app.use(router) app.use(i18n) - app.use(VueDOMPurifyHTML) app.mount('#app') }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-wh6m-h6f4-rjf4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-68927ghsaADVISORY
- github.com/abhinavxd/libredesk/commit/270347849943ac6a43e9fd6ebdc99c71841900ebghsax_refsource_MISCWEB
- github.com/abhinavxd/libredesk/security/advisories/GHSA-wh6m-h6f4-rjf4ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.