VYPR
Moderate severityOSV Advisory· Published Jan 22, 2026· Updated Jan 22, 2026

Tendenci has Authenticated Remote Code Execution via Pickle Deserialization

CVE-2026-23946

Description

Tendenci is an open source content management system built for non-profits, associations and cause-based sites. Versions 15.3.11 and below include a critical deserialization vulnerability in the Helpdesk module (which is not enabled by default). This vulnerability allows Remote Code Execution (RCE) by an authenticated user with staff security level due to using Python's pickle module in helpdesk /reports/. The original CVE-2020-14942 was incompletely patched. While ticket_list() was fixed to use safe JSON deserialization, the run_report() function still uses unsafe pickle.loads(). The impact is limited to the permissions of the user running the application, typically www-data, which generally lacks write (except for upload directories) and execute permissions. This issue has been fixed in version 15.3.12.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
tendenciPyPI
< 15.3.1215.3.12

Affected products

1

Patches

3
2ff0a4576149

replace pickle with json

https://github.com/tendenci/tendenciJenny QianJan 18, 2026via ghsa
1 file changed · +5 4
  • tendenci/apps/imports/views.py+5 4 modified
    @@ -1,5 +1,6 @@
     import os
    -import pickle
    +#import pickle
    +import json
     from django.contrib.auth.decorators import login_required
     from django.http import HttpResponseRedirect, HttpResponse, Http404
     from django.urls import reverse
    @@ -131,7 +132,7 @@ def user_upload_subprocess(request, sid,
             fd = default_storage.open(recap_path, 'r')
             content = fd.read()
             fd.close()
    -        recap_dict = pickle.loads(content)
    +        recap_dict = json.loads(content)
             recap_dict.update({'users_list': recap_dict['users_list'] +
                                             import_dict['users_list'],
                                'invalid_list': recap_dict['invalid_list'] +
    @@ -157,7 +158,7 @@ def user_upload_subprocess(request, sid,
                            'file_name': import_dict['file_name']}
     
         fd = default_storage.open(recap_path, 'w')
    -    cPickle.dump(recap_dict, fd)
    +    json.dump(recap_dict, fd)
         fd.close()
         # clear the recap_dict
         recap_dict = None
    @@ -196,7 +197,7 @@ def user_upload_recap(request, sid):
             content = fd.read()
             fd.close()
     
    -        recap_dict = pickle.loads(content)
    +        recap_dict = json.loads(content)
     
             output = BytesIO()
             export_wb = Workbook()
    
23d9fd85ab76

update save_query to have post data validated

https://github.com/tendenci/tendenciJenny QianJan 16, 2026via ghsa
2 files changed · +39 12
  • tendenci/apps/helpdesk/forms.py+26 1 modified
    @@ -6,6 +6,8 @@
     forms.py - Definitions of newforms-based forms for creating and maintaining
                tickets.
     """
    +import simplejson
    +from base64 import b64decode
     from django import forms
     from django.forms import widgets
     from django.conf import settings
    @@ -21,7 +23,7 @@
         from datetime import datetime as timezone
     
     from tendenci.apps.helpdesk.lib import send_templated_mail, safe_template_context
    -from tendenci.apps.helpdesk.models import Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC, CustomField, TicketCustomFieldValue, TicketDependency
    +from tendenci.apps.helpdesk.models import SavedSearch, Ticket, Queue, FollowUp, Attachment, IgnoreEmail, TicketCC, CustomField, TicketCustomFieldValue, TicketDependency
     from tendenci.apps.helpdesk import settings as helpdesk_settings
     from tendenci.apps.base.forms import CustomCatpchaField
     
    @@ -110,6 +112,29 @@ def save(self, *args, **kwargs):
             return super(EditTicketForm, self).save(*args, **kwargs)
     
     
    +class SavedSearchForm(forms.ModelForm):
    +    query_encoded = forms.CharField()
    +    
    +    class Meta:
    +        model = SavedSearch
    +        fields = ('title', 'shared',)
    +
    +    def clean_query_encoded(self):
    +        """Validate the json serialized query"""
    +        query_encoded = self.cleaned_data['query_encoded']
    +        try:
    +            query_dict = simplejson.loads(b64decode(str(query_encoded).encode()))
    +        except simplejson.errors.JSONDecodeError:
    +            raise forms.ValidationError(_('Invalid query_encoded.'))
    +        # validate the content of the query
    +        valid_keys = ['filtering', 'sorting', 'sortreverse', 'keyword', 'other_filter', 'search_string']
    +        for k in query_dict.keys():
    +            if k not in valid_keys:
    +                raise forms.ValidationError(_(f'{k} is not a valid parameter.'))
    +        
    +        return query_encoded
    +
    +
     class EditFollowUpForm(forms.ModelForm):
         def __init__(self, *args, **kwargs):
             "Filter not openned tickets here."
    
  • tendenci/apps/helpdesk/views/staff.py+13 11 modified
    @@ -28,6 +28,7 @@
     from django.utils.html import escape
     from django import forms
     from django.utils.encoding import smart_str
    +from django.views.decorators.http import require_http_methods
     import simplejson
     
     try:
    @@ -36,7 +37,7 @@
         from datetime import datetime as timezone
     
     from tendenci.apps.theme.shortcuts import themed_response as render_to_resp
    -from tendenci.apps.helpdesk.forms import TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm, EditFollowUpForm, TicketDependencyForm
    +from tendenci.apps.helpdesk.forms import SavedSearchForm, TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm, EditFollowUpForm, TicketDependencyForm
     from tendenci.apps.helpdesk.lib import send_templated_mail, query_to_dict, apply_query, safe_template_context
     from tendenci.apps.helpdesk.models import UserSettings, Ticket, Queue, FollowUp, TicketChange, PreSetReply, Attachment, SavedSearch, IgnoreEmail, TicketCC, TicketDependency, QueueMembership
     from tendenci.apps.helpdesk import settings as helpdesk_settings
    @@ -1220,18 +1221,19 @@ def month_name(m):
     run_report = staff_member_required(run_report)
     
     
    +@require_http_methods(["POST"])
     def save_query(request):
    -    title = request.POST.get('title', None)
    -    shared = request.POST.get('shared', False) in ['on', 'True', True, 'TRUE']
    -    query_encoded = request.POST.get('query_encoded', None)
    -
    -    if not title or not query_encoded:
    -        return HttpResponseRedirect(reverse('helpdesk_list'))
    -
    -    query = SavedSearch(title=title, shared=shared, query=query_encoded, user=request.user)
    -    query.save()
    +    form = SavedSearchForm(request.POST)
    +    if form.is_valid():
    +        encoded_query = form.save(commit=False)
    +        encoded_query.user =request.user
    +        encoded_query.query = form.cleaned_data['query_encoded']
    +        encoded_query.save()
    +        return HttpResponseRedirect('%s?saved_query=%s' % (reverse('helpdesk_list'), encoded_query.id))
    +
    +    # invalid form
    +    return HttpResponseRedirect(reverse('helpdesk_list'))
     
    -    return HttpResponseRedirect('%s?saved_query=%s' % (reverse('helpdesk_list'), query.id))
     save_query = staff_member_required(save_query)
     
     
    
63e1b84a5b16

replace pickle with json to deserialize

https://github.com/tendenci/tendenciJenny QianJan 16, 2026via ghsa
1 file changed · +4 3
  • tendenci/apps/helpdesk/views/staff.py+4 3 modified
    @@ -1056,10 +1056,11 @@ def run_report(request, report):
                 return HttpResponseRedirect(reverse('helpdesk_report_index'))
             if not (saved_query.shared or saved_query.user == request.user):
                 return HttpResponseRedirect(reverse('helpdesk_report_index'))
    -
    -        import pickle
    +    
    +        #import pickle
             from base64 import b64decode
    -        query_params = pickle.loads(b64decode(str(saved_query.query).encode()))
    +        #query_params = pickle.loads(b64decode(str(saved_query.query).encode()))
    +        query_params = simplejson.loads(b64decode(str(saved_query.query).encode()))
             report_queryset = apply_query(report_queryset, query_params)
     
         from collections import defaultdict
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.