Backup restore could run operating-system commands
High7.2
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
Time
24m
Cost
$0.3252k input · 29k output · 1.27M cached
Product
Snipe-IT
Affected versions
Before 8.7.0
Class
OS command injection CWE-78
Severity
High 7.2
Summary
Overview
Snipe-IT restore streamed the SQL entry from an uploaded backup archive into the MySQL or MariaDB command-line client. That client was started without the option that disables local commands, so parts of the SQL file could be interpreted as operating-system commands.
The caller has to be a superadministrator. Commands then run as the web application user. That account can read database credentials and the application key, and it can change files and data the application can write. The optional backup sanitizer is off by default.
Snipe-IT 8.7.0 adds the client option that stops local command interpretation.
Sequence
Attack path
01
Step 1
A superadministrator uploads a backup archive and starts a restore.
02
Step 2
Restore hands the SQL entry to the database client.
03
Step 3
The client is running in a mode that can treat parts of that file as operating-system commands.
04
Step 4
Those commands run as the Snipe-IT web application user. This is not an unauthenticated issue.
#poc
Proof of concept
The demonstration is withheld. It shows the class of backup the restore path accepted, not a turnkey exploit.
On Snipe-IT before 8.7.0, a superadministrator restore of a crafted backup could cause the database client to run operating-system commands. The same restore on 8.7.0 does not.
Archive layout, the command syntax inside the SQL, and a replayable backup are withheld. Operators should treat backup restore on every release before 8.7.0 as in scope and upgrade.
#exploit
Exploitation
The primitive is operating-system command execution as the web application user, and only after a superadministrator starts a restore. It is not an unauthenticated remote code-execution chain.
A command that can read the process environment or application files can expose database credentials and the application key. What happens after that depends on the host.
No exploit script, crafted backup, or scanner signature is published. The section exists so a defender gets the impact and the bounds.
#details
Technical details
Root cause: restore streamed the SQL member of an uploaded backup into the database command-line client without the flag that disables local client commands.
Affected configuration: Snipe-IT before 8.7.0. The backup sanitizer, which would have reduced this class of input, is optional and off by default. The caller must already be a superadministrator.
The fix in 8.7.0 starts that client with the flag that refuses local command interpretation.
Remediation
Fixed in Snipe-IT 8.7.0.
Upgrade to Snipe-IT 8.7.0 or later.
Until you can upgrade, keep backup upload and restore limited to people who already administer the server.
How Submersion found it
01
Step 1
The task was to find a user-controlled file or stream passed to an external program that can run local commands from the contents of that input.
02
Step 2
The model searched process execution, image handling, and the backup restore path.
03
Step 3
Restore streams the SQL from an uploaded backup into the database client. The process arguments are fixed.
04
Step 4
On the client used by the reference deployment, that stream can be read as local commands. The caller is a superadministrator.
05
Step 5
The model wrote up the backup-restore finding.
Basin was pointed at Snipe-IT. It received the application and a running environment. It did not receive a vulnerability description, an exploit, a patch diff, or a prior disclosure.
The CVE credits the Submersion AI Security Research Team.
Run log
Interactive trajectory
The recorded Basin run for this finding. Step through the model’s reasoning, the tools it called, and the raw output it read on the way to the vulnerability.
ModelBasin
TargetSnipe-IT
Reasoning steps37
Tool calls54
Run cost$0.32
User
Search this codebase for OS command injection. For every place a user-controlled file or stream is passed to an external program, determine whether that program can execute local commands from the contents of its input, even when the process arguments themselves are fixed. Report the finding end to end including the file, the required user, and the impact.
37 steps · 54 tool calls
Basin
Let me try to obtain a MySQL client to empirically test batch-mode system handling, and meanwhile inspect the web route chain.
Basin
MariaDB is installed. Now let me start it and empirically test whether the MySQL client executes OS commands from a piped SQL dump.
Basin
The MariaDB client executed system id from the piped dump. Let me confirm this rigorously and check which client variants do this.
Basin
MariaDB confirmed: system, \!, and source all execute from a piped SQL dump. Now let me check whether the actual MySQL client behaves the same.
Basin
Critical distinction confirmed: MariaDB client executes system from a piped dump; MySQL client 8.4 does not. Let me verify the deployment defaults that determine which binary is used, and the authorization on these routes.
Basin
The backups routes are nested inside the admin group (which adds authorize:superuser), so a superuser is required. Let me confirm the filename validation and do a final clean end-to-end verification with the MariaDB client.
Basin
Everything is confirmed. Let me clean up the test environment.
Basin
I have a complete, empirically verified picture. Here is the finding.
OS command injection via backup restore (mysql/mariadb client)
The one place in this codebase where a user-controlled stream is handed to an external program is the database restore flow. The external program is the mysql binary at $mysql_binary, and whether it runs local commands from the input depends on whether that binary is a MariaDB client or a MySQL client.
End-to-end chain
Upload — POST /backups/upload → SettingsController::postUploadBackup() (app/Http/Controllers/SettingsController.php, ~line 965) stores the attacker's zip under storage/app/backups/.
Piping — app/Console/Commands/RestoreFromBackup.php extracts the single SQL stream from the zip ($sql_contents = $za->getStream(...), line ~425) and opens the external program with fixed, escapeshellarg'd arguments (line ~459):
In the default web path (--sanitize-with-prefix is null), the raw dump is written verbatim into the client's stdin (lines ~496–507). The arguments are fixed and safe — the payload is entirely in the piped content, exactly the case the task targets.
Required user and preconditions
User: a superuser (the backups routes are nested inside the admin group in routes/web.php line 156, which adds the authorize:superuser permission check; the inner group only adds auth).
app.lock_passwords must be false (not demo/locked mode).
DB_DUMP_PATH (default /usr/local/bin, .env.example uses /usr/bin) must contain a mysql binary.
Does the program execute commands from its input?
I installed and tested both client families against a live server:
Client
Piped dump containing system id
Result
MariaDB client 13.0.2 (mariadb)
executes
uid=501(user)... printed; system touch /tmp/final-provecreated the file
MySQL client 8.4.0 (mysql)
syntax error
no execution
So the binary matters. In the reference Docker deployment (Dockerfile installs Debian mysql-client, which is the MariaDB client; docker-compose.yml uses mariadb:11.4.7), DB_DUMP_PATH=mysql points at a MariaDB client, so the flow is exploitable. MariaDB's client interprets system <cmd>, \! <cmd>, and source <file> from stdin even in batch mode; MySQL's client does not.
Impact
Arbitrary OS command execution as the web server (PHP-FPM/Apache) process user, reachable through the ordinary backup-restore feature. A superuser uploading a crafted zip whose SQL dump contains system <cmd> (or \! <cmd>) then hits the restore endpoint and the command runs as the PHP user. (The flow also runs db:wipe + migrate, so the same user can destroy/replace the database — but the OS command execution is the escalation beyond the app's admin model.)
Other external-program sites examined (no finding)
app/Console/Commands/Version.php — shell_exec('git ...'), passthru('npm run prod'): CLI-only, arguments are fixed prompts, no user-controlled file/stream.
upgrade.php — git/composer/artisan shell calls in a CLI upgrade script; not web-reachable.
app/Console/Commands/LdapTroubleshooter.php — builds ldapsearch with escapeshellarg'd values; CLI diagnostic.
app/Http/Controllers/StorageProxyController.php — fpassthru($stream) streams an S3 object to the response; pure PHP, no external program.
spatie/laravel-backupbackup:run (vendor, not installed here) — invokes mysqldump with DB connection config (not user-controlled content) and zips with PHP ZipArchive.
Suggested fix (for reference)
Reject/strip meta-commands from the dump before piping: filter out lines starting with system, \!, and source (and their uppercase variants) when writing to $pipes[0], or pipe through the existing SQLStreamer sanitizer even in the default (non-clean) path. Alternatively, use a MySQL client that does not interpret meta-commands in batch mode, or invoke mysql with --binary-mode/a dedicated import path that never parses client meta-commands.
Model and evaluation
Model
Basin
Task framing
The CVE describes a superadministrator backup restore that reaches operating-system commands.
Verifier
Restore of the crafted backup class runs commands before 8.7.0 and does not on 8.7.0.
No exploit, archive, or command syntax is published on this page.