Fixes GHSA-rrx8-xf85-mqwg, reported by @hhesenjan.
get_markdown() checked ALLOWED_URL_SCHEMES with a regular expression on its
input that only knew inline links. Reference links and images were never
checked, and an HTML entity in the scheme hid it from the expression, so a
forbidden scheme could reach an href or src.
The rendered HTML now goes through nh3 with url_schemes set to
ALLOWED_URL_SCHEMES, which covers every markdown syntax. The input check is
kept, so inline links still lose their scheme rather than being dropped.
Link and image titles and the fenced code language class are allowed
through. <br /> is now serialised as <br>, and tests/test_markdown.py is
updated for it.
Fixes GHSA-f4gw-75qw-v2hf, reported by @hhesenjan.
The templates passed the output of get_markdown() through urlizetrunc.
urlize is not HTML aware and rewrote the inside of attribute values, so a
crafted ticket could break out of an attribute and add an event handler to
the staff ticket page. It fires on Django 5.2 and earlier; 6.0 hardened
urlize.
urlizetrunc is removed from the description, the resolution and the
follow-up comments. Markdown already turns URLs into links. The urlize calls
in public_view_ticket.html run over plain text and are unchanged.
The tests parse the page and assert that no element carries an on*
attribute. They only fail without the fix on Django 5.2.
Nothing verifies either address, so one differing only in case identifies a
different mailbox as far as this endpoint can tell. Switching the lookup to
__iexact would read as a usability fix and would widen the collision it was
fixed for, so the exact match is deliberate and now tested.
Fixes GHSA-pc66-g337-vfmf, reported by @mansurmavlankulov.
UserTicketViewSet answers "the tickets submitted by the current user" by
matching Ticket.submitter_email against User.email, behind IsAuthenticated
alone. Neither field is verified and both can be empty, so an account with no
address was the submitter of every ticket with no address, and read them in
full through the API.
The model default for submitter_email is NULL, which does not collide, but
Django's EmailField form field returns "" for empty input and the ModelForm
stores that in the nullable column. Every ticket submitted through a form
without an address is therefore exposed, which is the ordinary path rather than
an edge case.
An empty address now returns nothing rather than matching, and empty submitter
addresses are excluded whatever the caller's address is, so neither side of the
comparison can be the sentinel.
This is not the per-queue gap fixed in GHSA-g9r9-54m2-m6g5. That one was about
the queue permission model not being consulted by three other viewsets, needed
a staff account and per-queue mode enabled. This one needs neither, and the
queryset was already scoped, just on a key that identifies nobody.
tests/test_user_tickets_api.py covers both directions, and pins the premise
separately: a ticket submitted through the form with the field left blank
stores "" rather than NULL, which is what makes the collision reachable.
Fixes GHSA-6pjw-93c8-87fj, reported by @VidhanThakur17.
The /raw/preset/ endpoint looked a PreSetReply up by primary key alone and
returned its body with the default text/html content type. Any staff member
could therefore read a reply attached to a queue they have no access to, and
a reply body holding markup was executed in the browser of whoever displayed
it, even though the only caller feeds the response into a textarea.
The body is now served as text/plain with nosniff so it cannot be interpreted
as a document, and a non numeric id, which used to raise a ValueError out of
the lookup, is a 404 like any other unknown id.
The lookup is restricted to replies that are either global or attached to one
of the queues accessible_queues() returns, so it follows can_access_queue()
rather than the wider set the queue pickers are offered. get_queues() would
have kept every queue accepting public submissions readable, which is the
distinction the public submission test pins down. A reply attached to several
readable queues is matched once, hence the distinct().
With per-queue permissions off, or for a superuser, the filter keeps every
reply, so the default configuration is unchanged.
ModelSerializer.create() passes validated_data to objects.create() with the
password still in it, so the INSERT carried the plain text and set_password()
only overwrote it in a second statement. The first row is already in the
write-ahead log, the binary log, the replicas and any query log by then, and
nothing that comes after removes it there.
Popping the password out, building the instance and hashing before the single
save keeps the plain text out of every statement. The regression test reads
back the SQL the request executed and looks for it.
Creating an account through the API was guarded by IsAdminUser alone, which
tests is_staff and nothing else. is_staff is the flag admitting an account to
the admin site, not the right to create users, so the endpoint accepted
requests that /admin/auth/user/add/ refuses from the same account. Adding
DjangoModelPermissions brings the model permission back into the check. It
reads the permission off the view queryset, so a project with its own
AUTH_USER_MODEL is covered too, and IsAdminUser stays alongside it so holding
the permission is not on its own a way in.
UserSerializer set the password with set_password(), which hashes whatever it
is handed. Every AUTH_PASSWORD_VALIDATOR a deployment configured therefore
applied everywhere a password is set except here. The serializer now runs
validate_password() against an unsaved user carrying the rest of the submitted
fields, so validators comparing a password to its own account have the username
and email to work from, and a rejected password comes back as a 400 on the
password field.
Deployments calling this endpoint with a staff-only account will start getting
403 and need the permission granted.
Reported by @VidhanThakur17.