Fixed XSS security vulnerabilities, and external HTML content is always processed with sanitizeHtml() to avoid direct use of innerHTML.

This commit is contained in:
likeaturtle 2026-02-23 10:53:48 +08:00
parent a4e25a0f09
commit 3d1e0dc71c
2 changed files with 180 additions and 3 deletions

View file

@ -0,0 +1,146 @@
# WebSocket Chat Security - XSS Protection
## Issue
XSS vulnerability via markdown rendering where LLM responses containing HTML could be rendered directly without sanitization.
## Solution
Implemented comprehensive XSS protection using DOMPurify library with strict configuration.
## Implementation Details
### 1. DOMPurify Integration
- **Library**: DOMPurify v3.0.8 (loaded from CDN)
- **Location**: `/pkg/channels/websocket/chat.html`
### 2. Security Configuration
```javascript
const DOMPURIFY_CONFIG = {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'strong', 'em', 'b', 'i', 'u', 'del', 's', 'code', 'pre',
'ul', 'ol', 'li',
'blockquote',
'a', 'img',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'span', 'div'
],
ALLOWED_ATTR: [
'href', 'title', 'alt', 'src',
'class', 'id'
],
ALLOW_DATA_ATTR: false,
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'],
KEEP_CONTENT: true
};
```
### 3. Security Features
#### Blocked Elements
- `<script>` tags - Prevents JavaScript execution
- `<style>` tags - Prevents CSS injection
- `<iframe>`, `<object>`, `<embed>` - Prevents content embedding attacks
- `<form>`, `<input>`, `<button>` - Prevents form injection
#### Blocked Attributes
All event handler attributes are blocked:
- `onerror`, `onload`, `onclick`
- `onmouseover`, `onfocus`, `onblur`
- And any other `on*` attributes
#### Allowed Safe Elements
Only safe HTML tags for markdown rendering:
- Headings: `h1-h6`
- Text formatting: `p`, `br`, `hr`, `strong`, `em`, `b`, `i`, `u`, `del`, `s`
- Code: `code`, `pre`
- Lists: `ul`, `ol`, `li`
- Quotes: `blockquote`
- Links and images: `a`, `img`
- Tables: `table`, `thead`, `tbody`, `tr`, `th`, `td`
- Containers: `span`, `div`
### 4. Implementation Points
All markdown rendering goes through the `sanitizeHtml()` function:
1. **Assistant Messages** (Line 1345-1348)
```javascript
if (sender === 'assistant' || sender === 'system') {
const rawHtml = marked.parse(content);
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
}
```
2. **System Messages Update** (Line 1368-1371)
```javascript
function updateReconnectMessage(newContent) {
const rawHtml = marked.parse(newContent);
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
}
```
3. **Retry Button Messages** (Line 1393-1396)
```javascript
const rawHtml = marked.parse(
t.clickToRetry + '\n\n**[' + t.retryButton + ']**'
);
const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml;
```
### 5. User Input Protection
User messages are rendered as plain text using `textContent` instead of `innerHTML`:
```javascript
} else {
// 用户消息保持纯文本
contentDiv.textContent = content;
}
```
## Testing
A test suite is provided in `xss_test.html` that validates protection against:
- Script tag injection
- Event handler injection (onclick, onerror, etc.)
- iframe/object/embed injection
- JavaScript protocol URLs
- SVG-based attacks
- Style injection
- Form injection
- And various bypass attempts
To run tests:
1. Open `xss_test.html` in a browser
2. All tests should pass
3. Check browser console for any JavaScript errors
## Attack Vectors Mitigated
✅ Direct script injection: `<script>alert('XSS')</script>`
✅ Image onerror: `<img src="x" onerror="alert('XSS')">`
✅ JavaScript URLs: `[link](javascript:alert('XSS'))`
✅ Event handlers: `<a onclick="alert('XSS')">Click</a>`
✅ SVG attacks: `<svg onload="alert('XSS')"></svg>`
✅ Iframe injection: `<iframe src="javascript:alert('XSS')"></iframe>`
✅ Object tags: `<object data="javascript:alert('XSS')"></object>`
✅ Form injection: `<form><input type="text"></form>`
✅ Style injection: `<style>body { display: none; }</style>`
## Maintenance
When updating the chat interface:
1. Always use `sanitizeHtml()` for any HTML content from external sources
2. Never use `innerHTML` directly with user/LLM content
3. Keep DOMPurify library updated to latest stable version
4. Review `DOMPURIFY_CONFIG` when adding new markdown features
## References
- [DOMPurify GitHub](https://github.com/cure53/DOMPurify)
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
- [Marked.js Documentation](https://marked.js.org/)

View file

@ -890,6 +890,37 @@
mangle: false mangle: false
}); });
// 配置 DOMPurify 以提供更强的 XSS 防护
const DOMPURIFY_CONFIG = {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'strong', 'em', 'b', 'i', 'u', 'del', 's', 'code', 'pre',
'ul', 'ol', 'li',
'blockquote',
'a', 'img',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'span', 'div'
],
ALLOWED_ATTR: [
'href', 'title', 'alt', 'src',
'class', 'id'
],
ALLOW_DATA_ATTR: false,
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onfocus', 'onblur'],
KEEP_CONTENT: true,
RETURN_DOM: false,
RETURN_DOM_FRAGMENT: false,
RETURN_TRUSTED_TYPE: false
};
// 创建安全的 sanitize 函数
function sanitizeHtml(html) {
return DOMPurify.sanitize(html, DOMPURIFY_CONFIG);
}
let ws; let ws;
const messagesDiv = document.getElementById('messages'); const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('messageInput'); const messageInput = document.getElementById('messageInput');
@ -1314,7 +1345,7 @@
if (sender === 'assistant' || sender === 'system') { if (sender === 'assistant' || sender === 'system') {
// AI 回复和系统消息渲染 Markdown // AI 回复和系统消息渲染 Markdown
const rawHtml = marked.parse(content); const rawHtml = marked.parse(content);
const cleanHtml = DOMPurify.sanitize(rawHtml); const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml; contentDiv.innerHTML = cleanHtml;
} else { } else {
// 用户消息保持纯文本 // 用户消息保持纯文本
@ -1337,7 +1368,7 @@
if (contentDiv) { if (contentDiv) {
// Render Markdown for system messages // Render Markdown for system messages
const rawHtml = marked.parse(newContent); const rawHtml = marked.parse(newContent);
const cleanHtml = DOMPurify.sanitize(rawHtml); const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml; contentDiv.innerHTML = cleanHtml;
} }
} }
@ -1362,7 +1393,7 @@
const rawHtml = marked.parse( const rawHtml = marked.parse(
t.clickToRetry + '\n\n**[' + t.retryButton + ']**' t.clickToRetry + '\n\n**[' + t.retryButton + ']**'
); );
const cleanHtml = DOMPurify.sanitize(rawHtml); const cleanHtml = sanitizeHtml(rawHtml);
contentDiv.innerHTML = cleanHtml; contentDiv.innerHTML = cleanHtml;
messageDiv.appendChild(contentDiv); messageDiv.appendChild(contentDiv);