Skip to content

Commit e9a9255

Browse files
JacobCoffeeclaude
andcommitted
fix: resolve XSS vulnerabilities in sponsor management templates
Replace innerHTML with DOM APIs (createElement/textContent) in three templates to prevent XSS via benefit names or search input: - sponsorship_list.html: live-search empty state - sponsorship_detail.html: benefit search dropdown - composer.html: addBenefit() dynamic DOM construction Also fix contract status string mismatch (awaiting_signature → awaiting signature) in sponsorship_detail.html. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0466f66 commit e9a9255

3 files changed

Lines changed: 58 additions & 26 deletions

File tree

apps/sponsors/templates/sponsors/manage/composer.html

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -789,19 +789,43 @@ <h2>Customize Benefits</h2>
789789
div.setAttribute('data-package', 'true');
790790
div.style.background = '#e8f4fd';
791791
}
792-
var tooltip = description ? '<span title="' + description.replace(/"/g, '&quot;') + '" style="cursor:help;color:#999;font-size:12px;margin-left:4px;">?</span>' : '';
793-
var actionHtml;
792+
var infoDiv = document.createElement('div');
793+
infoDiv.className = 'benefit-info';
794+
var nameSpan = document.createElement('span');
795+
nameSpan.className = 'benefit-name';
796+
nameSpan.textContent = name;
797+
infoDiv.appendChild(nameSpan);
798+
if (description) {
799+
var tooltipSpan = document.createElement('span');
800+
tooltipSpan.title = description;
801+
tooltipSpan.style.cssText = 'cursor:help;color:#999;font-size:12px;margin-left:4px;';
802+
tooltipSpan.textContent = '?';
803+
infoDiv.appendChild(tooltipSpan);
804+
}
805+
if (value) {
806+
var valueSpan = document.createElement('span');
807+
valueSpan.className = 'benefit-value';
808+
valueSpan.textContent = '$' + value.toLocaleString();
809+
infoDiv.appendChild(valueSpan);
810+
}
811+
div.appendChild(infoDiv);
812+
var actionDiv = document.createElement('div');
813+
actionDiv.className = 'benefit-action';
794814
if (isPackage) {
795-
actionHtml = '<span style="font-size:10px;color:#3776ab;font-weight:600;padding:2px 6px;background:#d0e4f5;border-radius:3px;" title="Included in package">Pkg</span>';
815+
var pkgSpan = document.createElement('span');
816+
pkgSpan.style.cssText = 'font-size:10px;color:#3776ab;font-weight:600;padding:2px 6px;background:#d0e4f5;border-radius:3px;';
817+
pkgSpan.title = 'Included in package';
818+
pkgSpan.textContent = 'Pkg';
819+
actionDiv.appendChild(pkgSpan);
796820
} else {
797-
actionHtml = '<button type="button" class="remove-btn" onclick="removeBenefit(' + id + ', ' + value + ');">x</button>';
798-
}
799-
div.innerHTML = '<div class="benefit-info">' +
800-
'<span class="benefit-name">' + name + '</span>' +
801-
tooltip +
802-
(value ? '<span class="benefit-value">$' + value.toLocaleString() + '</span>' : '') +
803-
'</div>' +
804-
'<div class="benefit-action">' + actionHtml + '</div>';
821+
var removeBtn = document.createElement('button');
822+
removeBtn.type = 'button';
823+
removeBtn.className = 'remove-btn';
824+
removeBtn.textContent = 'x';
825+
removeBtn.addEventListener('click', function() { removeBenefit(id, value); });
826+
actionDiv.appendChild(removeBtn);
827+
}
828+
div.appendChild(actionDiv);
805829
sel.appendChild(div);
806830

807831
currentTotal += value;

apps/sponsors/templates/sponsors/manage/sponsorship_detail.html

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ <h3 style="display:flex;justify-content:space-between;align-items:center;">Spons
179179
<div class="sp-field">
180180
<div class="sp-label">Contract</div>
181181
<div class="sp-value">
182-
<span class="tag {% if contract.status == 'draft' %}tag-gray{% elif contract.status == 'awaiting_signature' %}tag-gold{% elif contract.status == 'executed' %}tag-green{% else %}tag-red{% endif %}">
182+
<span class="tag {% if contract.status == 'draft' %}tag-gray{% elif contract.status == 'awaiting signature' %}tag-gold{% elif contract.status == 'executed' %}tag-green{% else %}tag-red{% endif %}">
183183
{{ contract.get_status_display }}
184184
</span>
185185
</div>
@@ -459,23 +459,18 @@ <h2>Benefits <span class="badge">{{ benefits|length }}</span></h2>
459459
}
460460

461461
function renderDropdown(filter) {
462-
var html = '';
462+
dropdown.textContent = '';
463463
var q = (filter || '').toLowerCase();
464464
var count = 0;
465465
options.forEach(function(opt) {
466466
if (q && opt.text.toLowerCase().indexOf(q) === -1) return;
467467
var highlighted = opt.value === select.value;
468-
html += '<div class="bs-opt' + (highlighted ? ' bs-sel' : '') + '" data-val="' + opt.value + '"'
469-
+ ' style="padding:7px 12px;font-size:13px;cursor:pointer;border-bottom:1px solid #f5f5f5;'
470-
+ (highlighted ? 'background:#e8f0fe;font-weight:600;' : '')
471-
+ '">' + opt.text + '</div>';
472-
count++;
473-
});
474-
if (!count) html = '<div style="padding:10px 12px;font-size:13px;color:#999;">No matching benefits</div>';
475-
dropdown.innerHTML = html;
476-
dropdown.style.display = 'block';
477-
478-
dropdown.querySelectorAll('.bs-opt').forEach(function(el) {
468+
var el = document.createElement('div');
469+
el.className = 'bs-opt' + (highlighted ? ' bs-sel' : '');
470+
el.setAttribute('data-val', opt.value);
471+
el.style.cssText = 'padding:7px 12px;font-size:13px;cursor:pointer;border-bottom:1px solid #f5f5f5;'
472+
+ (highlighted ? 'background:#e8f0fe;font-weight:600;' : '');
473+
el.textContent = opt.text;
479474
el.addEventListener('mousedown', function(e) {
480475
e.preventDefault();
481476
select.value = el.dataset.val;
@@ -488,7 +483,16 @@ <h2>Benefits <span class="badge">{{ benefits|length }}</span></h2>
488483
el.addEventListener('mouseleave', function() {
489484
el.style.background = el.classList.contains('bs-sel') ? '#e8f0fe' : '';
490485
});
486+
dropdown.appendChild(el);
487+
count++;
491488
});
489+
if (!count) {
490+
var empty = document.createElement('div');
491+
empty.style.cssText = 'padding:10px 12px;font-size:13px;color:#999;';
492+
empty.textContent = 'No matching benefits';
493+
dropdown.appendChild(empty);
494+
}
495+
dropdown.style.display = 'block';
492496
}
493497

494498
input.addEventListener('focus', function() { renderDropdown(input.value); });

apps/sponsors/templates/sponsors/manage/sponsorship_list.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,14 @@
201201
if (!emptyRow) {
202202
emptyRow = document.createElement('tr');
203203
emptyRow.id = 'live-search-empty';
204-
emptyRow.innerHTML = '<td colspan="9" style="text-align:center;padding:20px;color:#999;">No sponsorships match &ldquo;' + q.replace(/</g, '&lt;') + '&rdquo;</td>';
204+
var td = document.createElement('td');
205+
td.setAttribute('colspan', '9');
206+
td.style.cssText = 'text-align:center;padding:20px;color:#999;';
207+
td.textContent = 'No sponsorships match \u201C' + q + '\u201D';
208+
emptyRow.appendChild(td);
205209
rows[0].parentNode.appendChild(emptyRow);
206210
} else {
207-
emptyRow.innerHTML = '<td colspan="9" style="text-align:center;padding:20px;color:#999;">No sponsorships match &ldquo;' + q.replace(/</g, '&lt;') + '&rdquo;</td>';
211+
emptyRow.firstChild.textContent = 'No sponsorships match \u201C' + q + '\u201D';
208212
emptyRow.style.display = '';
209213
}
210214
} else if (emptyRow) {

0 commit comments

Comments
 (0)