KEVWire

Security intelligence, with context.

Response snapshot Oct 27, 2026, 01:13 UTC

Back to the wire
Permalink
VulnerabilityGHSA-qv4m-m73m-8hj7

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

```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().'_'.

At a glance

Exploited
Not known
Severity
Not scored yet
Chance of exploitation
Not estimated
Sources
1 source

What to do

Consult the exact GitHub advisory for affected versions, mitigation, and patch availability.

Read GitHub-reviewed advisory GHSA-qv4m-m73m-8hj7 (opens a new tab)

What happened

#### 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.

Continue reading

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:

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

```

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>**.

Affected package ranges reported by GitHub: notrinos/notrinos-erp: <= 1.0.0

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-qv4m-m73m-8hj7 (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-qv4m-m73m-8hj7, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 25). NotrinosERP: Authenticated arbitrary file upload leads to remote code execution via HRM employee "Documents" (doc_file). https://kevwire.com/record/GHSA-qv4m-m73m-8hj7

MLA

"NotrinosERP: Authenticated arbitrary file upload leads to remote code execution via HRM employee "Documents" (doc_file)." KEVWire, 25 Oct. 2026, kevwire.com/record/GHSA-qv4m-m73m-8hj7.

Chicago

KEVWire. "NotrinosERP: Authenticated arbitrary file upload leads to remote code execution via HRM employee "Documents" (doc_file)." Last modified October 25, 2026. https://kevwire.com/record/GHSA-qv4m-m73m-8hj7.

Evidence summary (plain text)

NotrinosERP: Authenticated arbitrary file upload leads to remote code execution via HRM employee "Documents" (doc_file) (Jul 10, 2026).
```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.
Sources:
GitHub-reviewed advisory GHSA-qv4m-m73m-8hj7: https://github.com/advisories/GHSA-qv4m-m73m-8hj7
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: GHSA-qv4m-m73m-8hj7.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 26, 2026, 20:55 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are not available yet. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail