|
203 | 203 | // ============================================================ |
204 | 204 | // Markdown rendering + code copy buttons. |
205 | 205 | // ============================================================ |
| 206 | + function sanitizeMarkdownHTML(html) { |
| 207 | + var tpl = document.createElement('template'); |
| 208 | + tpl.innerHTML = html || ''; |
| 209 | + var allowedTags = { |
| 210 | + A: true, B: true, BLOCKQUOTE: true, BR: true, CODE: true, DEL: true, |
| 211 | + EM: true, H1: true, H2: true, H3: true, H4: true, H5: true, H6: true, |
| 212 | + HR: true, I: true, LI: true, OL: true, P: true, PRE: true, S: true, |
| 213 | + SPAN: true, STRONG: true, TABLE: true, TBODY: true, TD: true, TH: true, |
| 214 | + THEAD: true, TR: true, UL: true |
| 215 | + }; |
| 216 | + var allowedAttrs = { |
| 217 | + A: { href: true, title: true }, |
| 218 | + CODE: { class: true }, |
| 219 | + SPAN: { class: true }, |
| 220 | + TH: { align: true }, |
| 221 | + TD: { align: true } |
| 222 | + }; |
| 223 | + function safeURL(value) { |
| 224 | + try { |
| 225 | + var u = new URL(value, window.location.origin); |
| 226 | + return u.protocol === 'http:' || u.protocol === 'https:' || u.protocol === 'mailto:'; |
| 227 | + } catch (_) { |
| 228 | + return false; |
| 229 | + } |
| 230 | + } |
| 231 | + function clean(node) { |
| 232 | + Array.from(node.childNodes).forEach(function(child) { |
| 233 | + if (child.nodeType === Node.TEXT_NODE) return; |
| 234 | + if (child.nodeType !== Node.ELEMENT_NODE) { |
| 235 | + child.remove(); |
| 236 | + return; |
| 237 | + } |
| 238 | + if (!allowedTags[child.tagName]) { |
| 239 | + child.replaceWith(document.createTextNode(child.textContent || '')); |
| 240 | + return; |
| 241 | + } |
| 242 | + Array.from(child.attributes).forEach(function(attr) { |
| 243 | + var name = attr.name.toLowerCase(); |
| 244 | + var tagAttrs = allowedAttrs[child.tagName] || {}; |
| 245 | + if (!tagAttrs[name]) { |
| 246 | + child.removeAttribute(attr.name); |
| 247 | + return; |
| 248 | + } |
| 249 | + if (name === 'href' && !safeURL(attr.value)) { |
| 250 | + child.removeAttribute(attr.name); |
| 251 | + } |
| 252 | + }); |
| 253 | + if (child.tagName === 'A' && child.getAttribute('href')) { |
| 254 | + child.setAttribute('rel', 'noopener noreferrer'); |
| 255 | + child.setAttribute('target', '_blank'); |
| 256 | + } |
| 257 | + clean(child); |
| 258 | + }); |
| 259 | + } |
| 260 | + clean(tpl.content); |
| 261 | + return tpl.innerHTML; |
| 262 | + } |
206 | 263 | function renderMarkdown(root) { |
207 | 264 | (root || document).querySelectorAll('.chat-msg__body.md[data-md]').forEach(function(el) { |
208 | 265 | if (el.dataset.rendered) return; |
209 | | - el.innerHTML = marked.parse(el.dataset.md || ''); |
| 266 | + el.innerHTML = sanitizeMarkdownHTML(marked.parse(el.dataset.md || '')); |
210 | 267 | el.dataset.rendered = '1'; |
211 | 268 | hydrateCodeBlocks(el); |
212 | 269 | }); |
|
513 | 570 | } |
514 | 571 | } catch (err) { |
515 | 572 | if (err.name !== 'AbortError') { |
516 | | - bot.body.innerHTML = '<span class="chat-error">Error: ' + (err.message || err) + '</span>'; |
| 573 | + bot.body.textContent = ''; |
| 574 | + var errSpan = document.createElement('span'); |
| 575 | + errSpan.className = 'chat-error'; |
| 576 | + errSpan.textContent = 'Error: ' + (err.message || err); |
| 577 | + bot.body.appendChild(errSpan); |
517 | 578 | } else { |
518 | 579 | bot.body.innerHTML = '<span class="chat-error">Stopped.</span>'; |
519 | 580 | } |
|
557 | 618 | var md = document.createElement('div'); |
558 | 619 | md.className = 'chat-msg__body md'; |
559 | 620 | md.dataset.md = final; |
560 | | - md.innerHTML = marked.parse(final); |
| 621 | + md.innerHTML = sanitizeMarkdownHTML(marked.parse(final)); |
561 | 622 | md.dataset.rendered = '1'; |
562 | 623 | hydrateCodeBlocks(md); |
563 | 624 | bot.body.replaceWith(md); |
|
572 | 633 | } else if (ev.event === 'error') { |
573 | 634 | try { |
574 | 635 | var e = JSON.parse(ev.data); |
575 | | - bot.body.innerHTML = '<span class="chat-error">Error: ' + (e.message || 'unknown') + '</span>'; |
| 636 | + bot.body.textContent = ''; |
| 637 | + var errEl = document.createElement('span'); |
| 638 | + errEl.className = 'chat-error'; |
| 639 | + errEl.textContent = 'Error: ' + (e.message || 'unknown'); |
| 640 | + bot.body.appendChild(errEl); |
576 | 641 | } catch (_) { |
577 | 642 | bot.body.innerHTML = '<span class="chat-error">Error</span>'; |
578 | 643 | } |
|
0 commit comments