-
-
Notifications
You must be signed in to change notification settings - Fork 665
Remove XSS potential using format_html helpers #2950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,12 +2,12 @@ | |
|
|
||
| from django import template | ||
| from django.template.defaultfilters import stringfilter | ||
| from django.utils.html import format_html | ||
| from django.utils.html import format_html_join, mark_safe | ||
|
|
||
| register = template.Library() | ||
|
|
||
|
|
||
| @register.filter(is_safe=True) | ||
| @register.filter() | ||
| @stringfilter | ||
| def render_email(value): | ||
| """Render an email address with obfuscated dots and at-sign using spans.""" | ||
|
|
@@ -16,8 +16,8 @@ def render_email(value): | |
| mailbox_tokens = mailbox.split(".") | ||
| domain_tokens = domain.split(".") | ||
|
|
||
| mailbox = "<span>.</span>".join(mailbox_tokens) | ||
| domain = "<span>.</span>".join(domain_tokens) | ||
| mailbox = format_html_join(mark_safe("<span>.</span>"), "{}", [(token,) for token in mailbox_tokens]) | ||
| domain = format_html_join(mark_safe("<span>.</span>"), "{}", [(token,) for token in domain_tokens]) | ||
|
|
||
| return format_html(f"{mailbox}<span>@</span>{domain}") | ||
| return mailbox + mark_safe("<span>@</span>") + domain | ||
|
Comment on lines
10
to
+22
|
||
| return None | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||
| """Django admin configuration for the sponsors app.""" | ||||||
|
|
||||||
| import contextlib | ||||||
| from textwrap import dedent | ||||||
|
|
||||||
| from django.contrib import admin | ||||||
| from django.contrib.contenttypes.admin import GenericTabularInline | ||||||
|
|
@@ -12,7 +13,7 @@ | |||||
| from django.template import Context, Template | ||||||
| from django.urls import path, reverse | ||||||
| from django.utils.functional import cached_property | ||||||
| from django.utils.html import mark_safe | ||||||
| from django.utils.html import format_html, format_html_join, mark_safe | ||||||
| from import_export import resources | ||||||
| from import_export.admin import ImportExportActionModelAdmin | ||||||
| from import_export.fields import Field | ||||||
|
|
@@ -296,12 +297,12 @@ def get_benefit_split(self, obj: SponsorshipPackage) -> str: | |||||
| for i, (name, pct) in enumerate(split): | ||||||
| pct_str = f"{pct:.0f}%" | ||||||
| widths.append(pct_str) | ||||||
| spans.append(f"<span title='{name}' style='background-color:{colors[i]}'>{pct_str}</span>") | ||||||
| spans.append((name, colors[i], pct_str)) | ||||||
| # define a style that will show our span elements like a single horizontal stacked bar chart | ||||||
| style = f"color:#fff;text-align:center;cursor:pointer;display:grid;grid-template-columns:{' '.join(widths)}" | ||||||
| split_bar = format_html_join("", "<span title='{}' style='background-color:{}'>{}</span>", spans) | ||||||
| # wrap it all up and put a bow on it | ||||||
| html = f"<div style='{style}'>{''.join(spans)}</div>" | ||||||
| return mark_safe(html) | ||||||
| return format_html("<div style='{}'>{}</div>", style, split_bar) | ||||||
|
|
||||||
|
|
||||||
| class SponsorContactInline(admin.TabularInline): | ||||||
|
|
@@ -325,7 +326,7 @@ class SponsorshipsInline(admin.TabularInline): | |||||
| def link(self, obj): | ||||||
| """Return a link to the sponsorship change page.""" | ||||||
| url = reverse("admin:sponsors_sponsorship_change", args=[obj.id]) | ||||||
| return mark_safe(f"<a href={url}>{obj.id}</a>") | ||||||
| return format_html("<a href='{}'>{}</a>", url, obj.id) | ||||||
|
|
||||||
|
|
||||||
| @admin.register(Sponsor) | ||||||
|
|
@@ -652,27 +653,25 @@ def get_readonly_fields(self, request, obj): | |||||
| def sponsor_link(self, obj): | ||||||
| """Return an HTML link to the sponsor's admin change page.""" | ||||||
| url = reverse("admin:sponsors_sponsor_change", args=[obj.sponsor.id]) | ||||||
| return mark_safe(f"<a href={url}>{obj.sponsor.name}</a>") | ||||||
| return format_html("<a href='{}'>{}</a>", url, obj.sponsor.name) | ||||||
|
|
||||||
| @admin.display(description="Estimated cost") | ||||||
| def get_estimated_cost(self, obj): | ||||||
| """Return the estimated cost HTML for customized sponsorships.""" | ||||||
| cost = None | ||||||
| html = "This sponsorship has not customizations so there's no estimated cost" | ||||||
|
||||||
| html = "This sponsorship has not customizations so there's no estimated cost" | |
| html = "This sponsorship has no customizations so there's no estimated cost" |
Copilot
AI
Mar 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
format_html_join() expects an iterable of tuples; passing benefits (a list of strings) will splat each string into characters and only the first character will be used for {}. Wrap each benefit as (benefit,) before joining.
Copilot
AI
Mar 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue as above: format_html_join() needs an iterable of tuples, but benefits is a list of strings. Wrap each benefit as a 1-tuple so the entire string is rendered.
Copilot
AI
Mar 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There’s an extra literal double-quote in the generated HTML (<li><b>{year}</b>:") which will show up in the admin output. Remove the stray " from the template string.
| <li><b>{year}</b>:" | |
| <li><b>{year}</b>: |
sethmlarson marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Mar 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This format_html() call is passing a single tuple as the first positional argument, so the rendered link will contain the tuple string (and the second {} will be left unresolved). Pass obj.value.url and obj.value as separate arguments.
| html = format_html("<a href='{}' target='_blank'>{}</a>", (obj.value.url, obj.value)) | |
| html = format_html("<a href='{}' target='_blank'>{}</a>", obj.value.url, obj.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
render_email()currently builds HTML by concatenating strings/SafeStrings, which produces a plainstrand will be auto-escaped when used as a template filter (regressing the rendered output). Build and return the final value viaformat_html(...)(and avoidmark_safeconstants, which will also trigger S308 now).