Fix security issues in tag editor widgets

- Fix case-sensitivity mismatch in remove_tags (use name__iexact)
- Fix XSS vulnerability by removing onclick attributes
- Use data attributes and event delegation instead
- Apply DOM APIs to prevent injection attacks

Co-authored-by: Nick Sweeting <pirate@users.noreply.github.com>
This commit is contained in:
claude[bot]
2025-12-30 19:18:41 +00:00
parent a648c17ec7
commit 03b96ef4ce
2 changed files with 58 additions and 16 deletions

View File

@@ -534,9 +534,13 @@ class SnapshotAdmin(SearchResultsAdminMixin, ConfigEditorMixin, BaseModelAdmin):
messages.warning(request, "No tags specified.") messages.warning(request, "No tags specified.")
return return
# Parse comma-separated tag names and find matching Tag objects # Parse comma-separated tag names and find matching Tag objects (case-insensitive)
tag_names = [name.strip() for name in tags_str.split(',') if name.strip()] tag_names = [name.strip() for name in tags_str.split(',') if name.strip()]
tags = list(Tag.objects.filter(name__in=tag_names)) tags = []
for name in tag_names:
tag = Tag.objects.filter(name__iexact=name).first()
if tag:
tags.append(tag)
print('[-] Removing tags', [t.name for t in tags], 'from Snapshots', queryset) print('[-] Removing tags', [t.name for t in tags], 'from Snapshots', queryset)
for obj in queryset: for obj in queryset:

View File

@@ -75,7 +75,7 @@ class TagEditorWidget(forms.Widget):
pills_html += f''' pills_html += f'''
<span class="tag-pill" data-tag="{self._escape(tag)}"> <span class="tag-pill" data-tag="{self._escape(tag)}">
{self._escape(tag)} {self._escape(tag)}
<button type="button" class="tag-remove-btn" onclick="removeTag_{widget_id}(this, '{self._escape(tag)}')">&times;</button> <button type="button" class="tag-remove-btn" data-tag-name="{self._escape(tag)}">&times;</button>
</span> </span>
''' '''
@@ -151,7 +151,7 @@ class TagEditorWidget(forms.Widget):
}}); }});
}}; }};
window.removeTag_{widget_id} = function(btn, tagName) {{ window.removeTag_{widget_id} = function(tagName) {{
currentTags_{widget_id} = currentTags_{widget_id}.filter(function(t) {{ currentTags_{widget_id} = currentTags_{widget_id}.filter(function(t) {{
return t.toLowerCase() !== tagName.toLowerCase(); return t.toLowerCase() !== tagName.toLowerCase();
}}); }});
@@ -166,13 +166,31 @@ class TagEditorWidget(forms.Widget):
var pill = document.createElement('span'); var pill = document.createElement('span');
pill.className = 'tag-pill'; pill.className = 'tag-pill';
pill.setAttribute('data-tag', tag); pill.setAttribute('data-tag', tag);
pill.innerHTML = escapeHtml(tag) +
'<button type="button" class="tag-remove-btn" onclick="removeTag_{widget_id}(this, \\'' + var tagText = document.createTextNode(tag);
escapeHtml(tag).replace(/'/g, "\\\\'") + '\\')">&times;</button>'; pill.appendChild(tagText);
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'tag-remove-btn';
removeBtn.setAttribute('data-tag-name', tag);
removeBtn.innerHTML = '&times;';
pill.appendChild(removeBtn);
container.appendChild(pill); container.appendChild(pill);
}}); }});
}}; }};
// Add event delegation for remove buttons
document.getElementById('{widget_id}_pills').addEventListener('click', function(event) {{
if (event.target.classList.contains('tag-remove-btn')) {{
var tagName = event.target.getAttribute('data-tag-name');
if (tagName) {{
removeTag_{widget_id}(tagName);
}}
}}
}});
window.handleTagKeydown_{widget_id} = function(event) {{ window.handleTagKeydown_{widget_id} = function(event) {{
var input = event.target; var input = event.target;
var value = input.value.trim(); var value = input.value.trim();
@@ -285,7 +303,7 @@ class InlineTagEditorWidget(TagEditorWidget):
pills_html += f''' pills_html += f'''
<span class="tag-pill" data-tag="{self._escape(td['name'])}" data-tag-id="{td['id']}"> <span class="tag-pill" data-tag="{self._escape(td['name'])}" data-tag-id="{td['id']}">
<a href="/admin/core/snapshot/?tags__id__exact={td['id']}" class="tag-link">{self._escape(td['name'])}</a> <a href="/admin/core/snapshot/?tags__id__exact={td['id']}" class="tag-link">{self._escape(td['name'])}</a>
<button type="button" class="tag-remove-btn" onclick="removeInlineTag_{widget_id}(event, {td['id']}, '{self._escape(td['name'])}')">&times;</button> <button type="button" class="tag-remove-btn" data-tag-id="{td['id']}" data-tag-name="{self._escape(td['name'])}">&times;</button>
</span> </span>
''' '''
@@ -362,10 +380,7 @@ class InlineTagEditorWidget(TagEditorWidget):
document.getElementById('{widget_id}_input').value = ''; document.getElementById('{widget_id}_input').value = '';
}}; }};
window.removeInlineTag_{widget_id} = function(event, tagId, tagName) {{ window.removeInlineTag_{widget_id} = function(tagId) {{
event.stopPropagation();
event.preventDefault();
fetch('/api/v1/core/tags/remove-from-snapshot/', {{ fetch('/api/v1/core/tags/remove-from-snapshot/', {{
method: 'POST', method: 'POST',
headers: {{ headers: {{
@@ -399,14 +414,37 @@ class InlineTagEditorWidget(TagEditorWidget):
pill.className = 'tag-pill'; pill.className = 'tag-pill';
pill.setAttribute('data-tag', td.name); pill.setAttribute('data-tag', td.name);
pill.setAttribute('data-tag-id', td.id); pill.setAttribute('data-tag-id', td.id);
pill.innerHTML = '<a href="/admin/core/snapshot/?tags__id__exact=' + td.id + '" class="tag-link">' +
escapeHtml(td.name) + '</a>' + var link = document.createElement('a');
'<button type="button" class="tag-remove-btn" onclick="removeInlineTag_{widget_id}(event, ' + link.href = '/admin/core/snapshot/?tags__id__exact=' + td.id;
td.id + ', \\'' + escapeHtml(td.name).replace(/'/g, "\\\\'") + '\\')">&times;</button>'; link.className = 'tag-link';
link.textContent = td.name;
pill.appendChild(link);
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'tag-remove-btn';
removeBtn.setAttribute('data-tag-id', td.id);
removeBtn.setAttribute('data-tag-name', td.name);
removeBtn.innerHTML = '&times;';
pill.appendChild(removeBtn);
container.appendChild(pill); container.appendChild(pill);
}}); }});
}}; }};
// Add event delegation for remove buttons
document.getElementById('{widget_id}_pills').addEventListener('click', function(event) {{
if (event.target.classList.contains('tag-remove-btn')) {{
event.stopPropagation();
event.preventDefault();
var tagId = parseInt(event.target.getAttribute('data-tag-id'), 10);
if (tagId) {{
removeInlineTag_{widget_id}(tagId);
}}
}}
}});
window.handleInlineTagKeydown_{widget_id} = function(event) {{ window.handleInlineTagKeydown_{widget_id} = function(event) {{
event.stopPropagation(); event.stopPropagation();
var input = event.target; var input = event.target;