diff --git a/WebApp/app.js b/WebApp/app.js
index 83f9d13..30b6bf4 100644
--- a/WebApp/app.js
+++ b/WebApp/app.js
@@ -207,13 +207,34 @@ function applyChatUI(cfg) {
// ─── Chat UI helpers ──────────────────────────────────────────
+/** Sanitise bot HTML: allow only …, escape everything else */
+function sanitizeBotHTML(raw) {
+ // 1. Escape the entire string first
+ const esc = raw
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+ // 2. Re-enable only safe tags
+ return esc.replace(
+ /<a\s+href="(https?:\/\/[^&"<>]+)"(?:\s+target="(_blank)")?>(.+?)<\/a>/gi,
+ (_, href, target, label) =>
+ `${label}`
+ );
+}
+
function appendBubble(type, text, actions) {
const msgs = document.getElementById('chat-messages');
if (!msgs) return;
const div = document.createElement('div');
div.className = `chat-bubble chat-bubble--${type}`;
div.setAttribute('role', 'article');
- div.textContent = text; // textContent only – no XSS (#6)
+ // Bot messages: render safe links; user/error messages: plain text only
+ if (type === 'bot' || type === 'error') {
+ div.innerHTML = sanitizeBotHTML(text);
+ } else {
+ div.textContent = text;
+ }
if (actions?.length) {
const row = document.createElement('div'); row.className = 'chat-fallback-actions';
diff --git a/WebApp/styles.css b/WebApp/styles.css
index 5305ab1..52b4ae3 100644
--- a/WebApp/styles.css
+++ b/WebApp/styles.css
@@ -565,6 +565,15 @@ button { cursor: pointer; font-family: inherit; border: none; background: none;
background: rgba(180,50,30,0.15); border: 1px solid rgba(180,50,30,0.3);
color: #F5C0B0; border-bottom-left-radius: 3px;
}
+.chat-bubble--bot a {
+ color: var(--c-accent-light, #F5A623);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ word-break: break-word;
+}
+.chat-bubble--bot a:hover {
+ color: var(--c-accent, #D4820A);
+}
.chat-typing {
align-self: flex-start; display: flex; gap: 4px; padding: 12px 14px;
background: var(--c-surface); border: 1px solid var(--c-border);