One-file Magento 2 patch-level and compromise check. Runs on the server, and deliberately installs nothing.
curl -sSL https://raw.githubusercontent.com/Siteation/magecheck/v7.1/magecheck.sh \
| bash -s -- -r /data/web/magento2Full sweep (persistence, database, access logs):
curl -sSL https://raw.githubusercontent.com/Siteation/magecheck/v7.1/magecheck.sh \
| bash -s -- -r /data/web/magento2 -p -d -lPipe to bash, not sh — the script needs bash and will tell you so.
Three problems solved by one decision:
- Managed hosting wipes stray directories. Hypernode removes unknown folders
under
/data/web, so any~/ir-toolsinstall evaporates. Streaming has nowhere to evaporate from. - Malware scanners flag it. The script contains attacker hashes and C2 hostnames as literal strings. On disk in the store tree it looks like malware — and in the wrong docroot layout it is publicly readable.
- A tool on a suspect host is a tool the attacker can edit. Fetching per run means you always execute the version you published, not the version that happens to be sitting on a possibly-compromised box.
Trade-off: it needs outbound HTTPS to GitHub, and you should read what you pipe into a shell. See Verifying below.
Add a function to ~/.bashrc — a file, not a directory, so managed hosts leave
it alone:
magecheck() {
curl -sSL https://raw.githubusercontent.com/Siteation/magecheck/v7.1/magecheck.sh \
| bash -s -- -r /data/web/magento2 "$@"
}Then magecheck or magecheck -p -d -l.
Push it from your laptop and clean up after:
scp magecheck.sh user@host:/tmp/.mc.$$ && \
ssh user@host 'bash /tmp/.mc.$$ -r /data/web/magento2; rm -f /tmp/.mc.$$'Or without touching disk at all:
ssh user@host 'bash -s -- -r /data/web/magento2' < magecheck.sh/tmp survives long enough for a run on every host worth scanning, and this
leaves nothing behind for a scanner to find.
Publish the hash with each release and check it before trusting a run:
curl -sSL <raw-url> | tee /tmp/mc.sh | sha256sum
# compare to the release, then: bash /tmp/mc.sh -r /data/web/magento2Pin the installer to a tag rather than main if more than one person can push.
| Section | |
|---|---|
| 1 | Version, and why security:patch-status is not evidence |
| 1b | VULN-39341 / APSB26-146 (CVE-2026-75650, StyleSmuggler) by code marker |
| 1c | 2026-09-001 September bundle — a separate, non-overlapping set |
| 1 | APSB26-73 (July) and APSB26-92 (August) |
| 2, 3 | Host persistence: cron, implant files, disguised processes |
| 4 | Poisoned logs and error reports, payment-failure mail bursts |
| 5 | Known payload hashes, webshell patterns |
| 6 | Live connections to known C2 |
| 7 | Access-log evidence (-l) |
| 8 | Database persistence (-d) |
| 9 | Code-change audit against git and composer.lock |
| 10 | Mitigation status, and which stop-gaps are now removable |
Flags: -p extended persistence, -d database, -l access logs, -q quiet,
-r <path> Magento root, -c <days> audit window, -u <url> canary target.
Exit codes: 0 patched, 1 indicators of compromise, 2 unpatched,
3 stop-gap mitigation only, 4 patch partially applied.
A clean run means nothing was visible to these checks. It is not a certificate of health, and the summary says so. Patch level is a fact; "no IOCs found" is an absence of evidence.
- Siteation/magento2-security-patches-249 — the patch sets themselves
- qoliber/magento-open-source-VULN-39341 — Adobe patches split per composer package, machine-verified
- sansec.io/research/stylesmuggler-0day — the campaign analysis these IOCs come from