Skip to content

Commit 3208a14

Browse files
committed
Fix XSS v2
1 parent a911b5d commit 3208a14

5 files changed

Lines changed: 60 additions & 35 deletions

File tree

Build/src/contextMenu.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ export class ContextMenuManager {
206206
try {
207207
const res = await fetch(`${this.app.baseURL}/mod/${this.app.elements.roomSelect.value}?user=${encodeURIComponent(this.app.user)}`, {
208208
method: "POST",
209-
headers: { "Content-Type": "application/json" },
209+
headers: this.app.getAuthHeaders(true), // Include Content-Type and auth headers
210210
body: JSON.stringify({
211211
action: 'kick',
212212
targetUser: this.currentMessage.user,
@@ -239,7 +239,7 @@ export class ContextMenuManager {
239239
try {
240240
const res = await fetch(`${this.app.baseURL}/mod/${this.app.elements.roomSelect.value}?user=${encodeURIComponent(this.app.user)}`, {
241241
method: "POST",
242-
headers: { "Content-Type": "application/json" },
242+
headers: this.app.getAuthHeaders(true), // Include Content-Type and auth headers
243243
body: JSON.stringify({
244244
action: 'ban',
245245
targetUser: this.currentMessage.user,

Build/src/main.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ class HTMLChatApp {
393393
this.elements.welcomeDiv.innerHTML = '';
394394

395395
// Create text node with safe content
396-
const welcomeText = document.createTextNode('Welcome to HTMLChat!, ');
396+
const welcomeText = document.createTextNode('Welcome to HTMLChat, ');
397397
const userBold = document.createElement('b');
398398
userBold.textContent = this.user;
399399
const middleText = document.createTextNode('! You are now in room ');
@@ -791,8 +791,9 @@ class HTMLChatApp {
791791
this.elements.roomSelect.value
792792
}?user=${encodeURIComponent(this.user)}`;
793793
try {
794-
navigator.sendBeacon(url, JSON.stringify({ method: "DELETE" }));
794+
await fetch(url, { method: "DELETE", keepalive: true });
795795
} catch (e) {
796+
// Fallback: try again without awaiting in case of network issues
796797
fetch(url, { method: "DELETE", keepalive: true }).catch(() => {});
797798
}
798799
}

Build/src/messageRenderer.js

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export class MessageRenderer {
1717
div.textContent = text;
1818
return div.innerHTML;
1919
}
20+
21+
// Escape for attribute contexts (adds quote escaping)
22+
escapeAttr(s) {
23+
return this.escapeHtml(String(s))
24+
.replace(/"/g, '"')
25+
.replace(/'/g, ''');
26+
}
2027

2128
// Helper method to create lucide icons
2229
createIcon(iconName, options = {}) {
@@ -104,11 +111,20 @@ export class MessageRenderer {
104111
'b','i','em','strong','u','a','p','ul','ol','li','code',
105112
'pre','img','h1','h2','h3','h4','h5','h6','br','span','div'
106113
],
107-
ALLOWED_ATTR: ['href','src','alt','title','target','style']
114+
ALLOWED_ATTR: ['href','src','alt','title','target','style','rel']
108115
});
109116

110117
// 3. Convert remaining plain URLs into clickable links
111-
html = html.replace(/(?<!["'>])\bhttps?:\/\/[^\s<]+/g, '<a href="$&" target="_blank" rel="noopener noreferrer" style="color:#0066cc">$&</a>');
118+
html = html.replace(/(?<!["'>])\bhttps?:\/\/[^\s<]+/g, (url) => {
119+
const safeHref = this.escapeAttr(url);
120+
const safeText = this.escapeHtml(url);
121+
return `<a href="${safeHref}" target="_blank" rel="noopener noreferrer" style="color:#0066cc">${safeText}</a>`;
122+
});
123+
// Defense-in-depth: sanitize again
124+
html = DOMPurify.sanitize(html, {
125+
ALLOWED_TAGS: ['b','i','em','strong','u','a','p','ul','ol','li','code','pre','img','h1','h2','h3','h4','h5','h6','br','span','div'],
126+
ALLOWED_ATTR: ['href','src','alt','title','target','style','rel']
127+
});
112128

113129
return html;
114130
}
@@ -166,52 +182,52 @@ export class MessageRenderer {
166182
if (message.system) messageClass += ' system';
167183

168184
let messageHtml = `
169-
<div class="${messageClass}" id="${this.escapeHtml(messageId)}"
170-
data-user="${this.escapeHtml(user)}"
171-
data-time="${this.escapeHtml(time)}"
172-
data-message-id="${this.escapeHtml(messageId)}">
185+
<div class="${messageClass}" id="${this.escapeAttr(messageId)}"
186+
data-user="${this.escapeAttr(user)}"
187+
data-time="${this.escapeAttr(String(time))}"
188+
data-message-id="${this.escapeAttr(messageId)}">
173189
`;
174190

175191
// Add reply reference if this is a reply (before timestamp and user)
176192
if (replyInfo) {
177193
messageHtml += `
178-
<div class="reply-reference" data-message-id="${this.escapeHtml(replyInfo.messageId)}">
194+
<div class="reply-reference" data-message-id="${this.escapeAttr(replyInfo.messageId)}">
179195
↳ Replying to ${this.escapeHtml(replyInfo.replyUser)}
180196
</div>
181197
`;
182198
}
183199

184200
messageHtml += `
185201
<span class="time">[${this.escapeHtml(date)}]</span>
186-
<span class="user${isModerator ? ' moderator' : ''}"
187-
style="color:${this.escapeHtml(color)}"
188-
data-user="${this.escapeHtml(user)}">&lt;${this.escapeHtml(user)}&gt;</span>
202+
<span class="user${isModerator ? ' moderator' : ''}"
203+
style="color:${this.escapeAttr(color)}"
204+
data-user="${this.escapeAttr(user)}">&lt;${this.escapeHtml(user)}&gt;</span>
189205
`;
190206

191207
// Add the message content
192208
if (fileAttachment) {
193209
if (fileAttachment.type.startsWith('image/')) {
194-
const imageUrl = this.escapeHtml(fileAttachment.url || fileAttachment.data);
195-
const imageName = this.escapeHtml(fileAttachment.name);
210+
const imageUrl = this.escapeAttr(fileAttachment.url || fileAttachment.data);
211+
const imageName = this.escapeAttr(fileAttachment.name);
196212
const uploadedBy = this.escapeHtml(fileAttachment.uploadedBy || 'Unknown');
197213
const uploadedAt = fileAttachment.uploadedAt ? this.escapeHtml(new Date(fileAttachment.uploadedAt).toLocaleString()) : '';
198214
const titleText = `Uploaded by ${uploadedBy}${uploadedAt ? ' on ' + uploadedAt : ''}`;
199215

200216
messageHtml += `
201217
<span class="text">
202-
<img src="${imageUrl}"
203-
alt="${imageName}"
218+
<img src="${imageUrl}"
219+
alt="${imageName}"
204220
class="image-attachment clickable-image"
205221
data-url="${imageUrl}"
206-
title="${this.escapeHtml(titleText)}">
222+
title="${this.escapeAttr(titleText)}">
207223
</span>
208224
`;
209225
} else {
210226
const iconName = this.getFileIconName(fileAttachment.type);
211227
const iconHtml = this.createIcon(iconName, {
212228
style: { width: '16px', height: '16px', marginRight: '4px' }
213229
});
214-
const fileUrl = this.escapeHtml(fileAttachment.url || fileAttachment.data);
230+
const fileUrl = this.escapeAttr(fileAttachment.url || fileAttachment.data);
215231
const fileName = this.escapeHtml(fileAttachment.name);
216232
const uploadedBy = this.escapeHtml(fileAttachment.uploadedBy || 'Unknown');
217233
const uploadedAt = fileAttachment.uploadedAt ? this.escapeHtml(new Date(fileAttachment.uploadedAt).toLocaleString()) : '';
@@ -220,12 +236,12 @@ export class MessageRenderer {
220236

221237
messageHtml += `
222238
<span class="text">
223-
<a href="${fileUrl}"
224-
${fileAttachment.filename ? `download="${fileName}"` : 'target="_blank" rel="noopener noreferrer"'}
239+
<a href="${fileUrl}"
240+
${fileAttachment.filename ? `download="${this.escapeAttr(fileName)}"` : 'target="_blank" rel="noopener noreferrer"'}
225241
class="file-attachment"
226-
title="${this.escapeHtml(titleText)}">
242+
title="${this.escapeAttr(titleText)}">
227243
${iconHtml}
228-
${fileName} (${this.escapeHtml(fileSize)})
244+
${this.escapeHtml(fileName)} (${this.escapeHtml(fileSize)})
229245
</a>
230246
</span>
231247
`;

Build/src/privateMessages.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export class PrivateMessageManager {
225225
// Send PM to server - encode conversationId to handle special characters
226226
const res = await fetch(`${this.app.baseURL}/pm/${encodeURIComponent(conversationId)}?user=${encodeURIComponent(this.app.user)}`, {
227227
method: 'POST',
228-
headers: { 'Content-Type': 'application/json' },
228+
headers: this.app.getAuthHeaders(true), // Include Content-Type and auth headers
229229
body: JSON.stringify({ text: message, to: username })
230230
});
231231

@@ -285,7 +285,9 @@ export class PrivateMessageManager {
285285
const conversationId = [this.app.user, username].sort().join('_');
286286

287287
// Fetch from server - encode conversationId to handle special characters
288-
const res = await fetch(`${this.app.baseURL}/pm/${encodeURIComponent(conversationId)}?user=${encodeURIComponent(this.app.user)}`);
288+
const res = await fetch(`${this.app.baseURL}/pm/${encodeURIComponent(conversationId)}?user=${encodeURIComponent(this.app.user)}`, {
289+
headers: this.app.getAuthHeaders(false) // No Content-Type for GET requests
290+
});
289291

290292
if (res.ok) {
291293
const data = await res.json();

Build/src/search.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,23 @@ export class SearchManager {
107107
const chunkSize = 50;
108108
let filteredMessages = [];
109109

110+
// Precompute username filter needle once for performance
111+
const usernameFilterNeedle = this.userFilter.checked && this.usernameFilter.value.trim()
112+
? this.usernameFilter.value.trim().toLowerCase()
113+
: null;
114+
110115
for (let i = 0; i < messages.length; i += chunkSize) {
111116
const chunk = messages.slice(i, i + chunkSize);
112117

113118
const chunkFiltered = chunk.filter(msg => {
114-
// Text search
115-
const textMatch = msg.text.toLowerCase().includes(query) ||
116-
msg.user.toLowerCase().includes(query);
119+
// Text search with null-safety
120+
const safeText = String(msg.text || '').toLowerCase();
121+
const safeUser = String(msg.user || '').toLowerCase();
122+
const textMatch = safeText.includes(query) || safeUser.includes(query);
117123

118124
// User filter
119-
if (this.userFilter.checked && this.usernameFilter.value.trim()) {
120-
const userMatch = msg.user.toLowerCase().includes(this.usernameFilter.value.trim().toLowerCase());
125+
if (usernameFilterNeedle) {
126+
const userMatch = safeUser.includes(usernameFilterNeedle);
121127
return textMatch && userMatch;
122128
}
123129

@@ -153,11 +159,11 @@ export class SearchManager {
153159

154160
const html = messages.map((msg, index) => {
155161
const date = new Date(msg.time).toLocaleString();
156-
const color = this.app.messageRenderer.getUserColor(msg.user);
162+
const color = this.app.messageRenderer.getUserColor(msg.user || '');
157163

158-
// Highlight search terms
159-
let highlightedText = this.highlightSearchTerms(msg.text, query);
160-
let highlightedUser = this.highlightSearchTerms(msg.user, query);
164+
// Highlight search terms with null-safety
165+
let highlightedText = this.highlightSearchTerms(msg.text || '', query);
166+
let highlightedUser = this.highlightSearchTerms(msg.user || '', query);
161167

162168
const messageId = `msg-${msg.time}-${index}`;
163169

0 commit comments

Comments
 (0)