Are CodeCanyon PHP Scripts Safe? A Security Audit Checklist Calibrated on 1.67 Million Lines
Published 5 September 2026. The scanner below was run against 1,674,891 lines of production PHP on a live server. Every count in this article is a measurement, not an estimate.
1. The Verdict, and What the Scan Actually Found
CodeCanyon scripts are not audited for security by Envato. The marketplace review checks that an item works, is documented and does not obviously contain malware. It does not check for SQL injection, missing CSRF tokens, unindexed queries that lock your database at scale, or a licence routine that calls home with your server details. Treating a purchase receipt as a security assurance is the mistake this article exists to prevent.
The usual advice is to grep for eval and base64_decode. We ran exactly that against 7,441 PHP files totalling 1,674,891 lines of reputable, widely deployed code, and it produced 588 hits. Every single one was benign. Three of the five eval hits were inside comments. Two of the two preg_replace /e hits were the regex matching the word “textarea”. That is the signal-to-noise ratio you are actually working with.
Meanwhile, a four-line backdoor we wrote to test the scanner evaded every one of those patterns, because it never writes the word eval anywhere. The checklist most people publish would have passed it.
What follows is the version that works: a layered scan where the noisy patterns build an inventory, combination patterns catch the obvious cases, and one file-level heuristic reduced 7,441 files to 11 worth opening by hand. If you are deploying the script afterwards, the server-side companion to this article is our CodeCanyon deployment guide with production benchmarks, and the wider context is in our self-hosted SaaS architecture guide.
Risk Assessment Matrix
| Risk | Detectable by grep? | Likelihood in a paid marketplace script | Blast radius |
|---|---|---|---|
| Deliberate backdoor | Only if unobfuscated | Low in paid items, high in nulled copies | Full server compromise |
| Licence phone-home | Yes, curl_exec to a fixed host | High, and usually disclosed | Metadata leak, breaks offline |
| SQL injection | Partially | Moderate | Database read/write |
| Missing CSRF tokens | No, requires live testing | Moderate | Account takeover |
| Unrestricted file upload | Partially | Moderate | Remote code execution |
| Unindexed queries | No, requires load | High | Outage under growth |
| Abandoned dependencies | Yes, composer audit | High | Varies by CVE |
Two rows deserve emphasis. Nulled copies are a categorically different risk from paid ones: the entire economic purpose of distributing a cracked script is to monetise the installs, and the payload is the product. And unindexed queries are the risk most likely to actually hurt you, because it is the one that arrives on your busiest day rather than on day one.
2. What the Marketplace Review Does and Does Not Cover
Envato’s review is a quality gate, not a penetration test. Understanding the boundary tells you exactly which work remains yours.
| Check | Marketplace review | Your responsibility |
|---|---|---|
| Item installs and runs | Yes | Verify on your stack |
| Documentation exists | Yes | — |
| Obvious malware signature | Automated scan | Re-scan yourself |
| Injection and CSRF testing | No | Entirely yours |
| Dependency CVE audit | No | Entirely yours |
| Database schema and indexing | No | Entirely yours |
| Continued patching after purchase | Author’s discretion | Monitor, or fork |
The last row is the one that bites over time. A regular licence includes six months of support and updates for the major version you bought. An author who stops publishing leaves you owning code with no upstream, and the practical response is to budget for maintaining it yourself rather than to assume patches will keep arriving.
Buy the licence. Always.
A nulled script is not the same product with the licence check removed. Someone spent effort cracking, repackaging and distributing it, and that effort is funded by what they added. The most common additions are an authenticated backdoor keyed to a parameter only the distributor knows, an outbound beacon registering your install, and injected SEO links served only to search engine crawlers so that you never see them in a browser.
The economics are not close. A regular licence is typically $39 to $79. The cheapest possible incident response — restoring from backup, rotating every credential the server touched, and auditing what left the machine — is a day of work. Our review of production-ready CodeCanyon SaaS scripts covers which items we found worth their licence fee.
Pre-purchase signals that cost nothing to check
The cheapest audit happens before the transaction, on the item page itself. None of these signals is decisive alone; together they separate maintained products from abandoned ones.
- Last update date against the changelog. An item last updated eighteen months ago, on a stack where PHP ships two minor versions a year, is code you will be maintaining. Check whether the changelog mentions the PHP version you intend to run.
- The comments tab, sorted to the newest. Ignore the ratings. Read whether the author answers, how fast, and what they answer. An author who responds “please contact support” to a technical question has outsourced their own product.
- Unanswered security questions. A buyer asking about a vulnerability with no reply, still visible months later, tells you both about the bug and about the response process.
- Stated requirements. An item that still lists PHP 7.4 as recommended in 2026 has not been tested on a supported runtime. PHP 7.4 reached end of security support in 2022.
- Demo behaviour. Open the demo with developer tools recording the network tab. Requests to domains unrelated to the demo host, before you have interacted with anything, are worth understanding.
Then buy through the marketplace, download through your own account, and verify what you received before unpacking it anywhere near a web root:
# Record what you received, before touching it
sha256sum ~/Downloads/item.zip | tee ~/item-received.sha256
# Inspect the archive without extracting: any PHP inside an assets, images
# or uploads path is a question to answer before anything runs.
unzip -l ~/Downloads/item.zip | grep -E '\.(php|phtml|phar)$' \
| grep -Ei '(asset|image|img|upload|media|cache|tmp)/'
# Extract to a directory no web server serves
install -d -m 700 /srv/staging/item
unzip -q ~/Downloads/item.zip -d /srv/staging/item
# Size sanity: an archive far larger than its file count suggests
# is worth opening before it is worth running.
du -sh /srv/staging/item
find /srv/staging/item -name '*.php' | wc -l
Unpacking outside a web root is not paranoia about this particular script. It is that an installer which writes a file into a served directory during extraction has already executed on your terms rather than yours, and you want that to be impossible rather than unlikely.
3. The Audit Script
Save this as /usr/local/bin/php-audit. It is the exact script whose output appears in section 5, run against the corpus described there.
#!/bin/bash
# Static audit of third-party PHP. Reports patterns worth reviewing,
# NOT confirmed vulnerabilities. Every result requires human triage.
set -uo pipefail
TARGET="${1:?usage: php-audit /path/to/code}"
EXCLUDE="${2:-/vendor/}"
scan() {
local label="$1" pattern="$2" n hits
n=$(grep -rEIl --include='*.php' "$pattern" "$TARGET" 2>/dev/null | grep -vc "$EXCLUDE" || true)
hits=$(grep -rEIn --include='*.php' "$pattern" "$TARGET" 2>/dev/null | grep -v "$EXCLUDE" | wc -l)
printf "%-34s %6s hits %5s files\n" "$label" "$hits" "$n"
}
echo "=== TARGET: $TARGET ==="
scan "eval()" 'eval[[:space:]]*\('
scan "base64_decode()" 'base64_decode[[:space:]]*\('
scan "gzinflate/gzuncompress" 'gz(inflate|uncompress)[[:space:]]*\('
scan "str_rot13()" 'str_rot13[[:space:]]*\('
scan "create_function()" 'create_function[[:space:]]*\('
scan "shell_exec/system/passthru" '(shell_exec|passthru|proc_open|popen)[[:space:]]*\('
scan "exec()" '[^_a-z]exec[[:space:]]*\('
scan "curl_exec()" 'curl_exec[[:space:]]*\('
scan "fsockopen()" 'fsockopen[[:space:]]*\('
scan "move_uploaded_file()" 'move_uploaded_file[[:space:]]*\('
scan "unserialize()" 'unserialize[[:space:]]*\('
scan "extract()" '[^_a-z]extract[[:space:]]*\('
echo "--- high-signal combinations ---"
scan "eval(base64_decode(...))" 'eval[[:space:]]*\([[:space:]]*(base64_decode|gzinflate|str_rot13|gzuncompress)'
scan "eval on superglobal" 'eval[[:space:]]*\(.*\$_(GET|POST|REQUEST|COOKIE)'
scan "exec on superglobal" '(shell_exec|system|passthru|exec)[[:space:]]*\(.*\$_(GET|POST|REQUEST|COOKIE)'
echo "--- file-level heuristic ---"
# Files that BOTH decode data AND call functions dynamically. This is the
# check that catches obfuscation which never spells out "eval".
comm -12 \
<(grep -rEIl --include='*.php' '(base64_decode|gzinflate|str_rot13|gzuncompress|hex2bin)[[:space:]]*\(' \
"$TARGET" 2>/dev/null | grep -v "$EXCLUDE" | sort) \
<(grep -rEIl --include='*.php' '\$[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(' \
"$TARGET" 2>/dev/null | grep -v "$EXCLUDE" | sort) \
| tee /tmp/php-audit-priority.txt | sed 's|^| |'
echo " -> $(wc -l < /tmp/php-audit-priority.txt) file(s) to open by hand"
chmod 700 /usr/local/bin/php-audit
php-audit /var/www/your-script
The /vendor/ exclusion is deliberate and is also a trade-off. Composer dependencies are enormous, are usually upstream code you did not write, and dominate the output if included. They are also a legitimate place to hide a payload in a repackaged script. Run the audit once with the exclusion to review the author’s own code, then once without it, and compare the vendor tree against a clean composer install of the same versions rather than reading it line by line.
4. The Manual Checks Grep Cannot Perform
CSRF enforcement
No static pattern proves a token is validated. Replay a state-changing request with the token removed and observe what happens:
# With a valid session cookie, no CSRF token
curl -i -X POST https://staging.example.com/account/email \
-H "Cookie: PHPSESSID=YOUR_VALID_SESSION" \
-d "email=attacker@example.com"
# Expected: 403, 419, or a redirect with the value UNCHANGED.
# A 200 or a redirect with the value CHANGED means no CSRF protection
# on that route, regardless of what the sales page claimed.
# Then confirm the change did not persist:
curl -s https://staging.example.com/account -H "Cookie: PHPSESSID=YOUR_VALID_SESSION" \
| grep -o 'attacker@example.com' && echo "VULNERABLE"
File upload validation
The failure mode is a handler that trusts the client-supplied MIME type. Test it with a file whose declared type and actual content disagree:
# A file that is PHP but announces itself as an image
printf '%s' 'GIF89a' > /tmp/probe.gif.php
echo '<?php echo "executed"; ?>' >> /tmp/probe.gif.php
curl -i -X POST https://staging.example.com/upload \
-H "Cookie: PHPSESSID=YOUR_VALID_SESSION" \
-F "file=@/tmp/probe.gif.php;type=image/gif"
# If the upload is accepted, find where it landed and whether it executes:
curl -s https://staging.example.com/uploads/probe.gif.php | grep -q executed \
&& echo "REMOTE CODE EXECUTION" || echo "not executed"
rm /tmp/probe.gif.php
Acceptance alone is not the vulnerability; execution is. The server-side mitigation is to deny PHP execution inside writable directories, which belongs in your nginx configuration whether or not the script validates properly:
location ~* ^/(uploads|storage|media|cache)/.*\.(php|phtml|phar)$ {
deny all;
}
Where the script phones home
Static analysis finds the call sites. Only a packet capture tells you what actually leaves the machine:
# Every outbound HTTP call site, with its destination
grep -rEn --include='*.php' \
"(curl_setopt.*CURLOPT_URL|file_get_contents[[:space:]]*\([[:space:]]*['\"]https?://|fsockopen)" \
/var/www/your-script | grep -v "/vendor/"
# Then watch the wire during install and first use, on staging
tcpdump -i any -n -A 'tcp port 80 or tcp port 443' -w /tmp/install.pcap &
TCPDUMP_PID=$!
# ... run the installer and exercise the application ...
kill $TCPDUMP_PID
tcpdump -r /tmp/install.pcap -n | awk '{print $3, $5}' | sort -u | head -40
A licence validation call to the author’s own domain is expected and usually documented. What matters is what it carries. A request containing your database name, your admin email or your directory paths is exfiltration wearing a licence check’s clothing.
Schema and indexing
The risk that most reliably causes an outage is not a backdoor. It is a query with no usable index on a table that was small during the demo:
# Tables with no index other than the primary key
mysql script_db -e "
SELECT t.TABLE_NAME, t.TABLE_ROWS
FROM information_schema.TABLES t
LEFT JOIN information_schema.STATISTICS s
ON t.TABLE_NAME = s.TABLE_NAME
AND t.TABLE_SCHEMA = s.TABLE_SCHEMA
AND s.INDEX_NAME != 'PRIMARY'
WHERE t.TABLE_SCHEMA = 'script_db' AND s.INDEX_NAME IS NULL;"
# Then generate realistic volume and capture what is slow
mysql -e "SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';"
# ... exercise the application under load ...
mysql -e "SET GLOBAL slow_query_log = 'OFF';"
awk '/Query_time/{print}' /var/log/mysql/slow.log | sort -rn | head -20
Dependencies
cd /var/www/your-script
composer audit # known CVEs in the dependency tree
composer outdated --direct # how far behind upstream the author is
composer show --installed | grep -Ei "phpunit|faker|ignition|debug|dev"
# Dev packages shipped in a distributed archive mean the author zipped their
# working directory. Assume the same carelessness elsewhere in the codebase.
5. Calibration: What the Scanner Actually Reports on Clean Code
Methodology. The script in section 3 was run against a live production WordPress installation on Ubuntu 24.04: core plus six widely deployed plugins from the official directory, all reputable and none suspected of anything. Corpus: 7,441 PHP files, 1,674,891 lines. Scan time: 6.4 seconds wall clock. The point of scanning known-good code is to establish the false-positive rate before you point the tool at something you actually distrust.
| Pattern | Hits | Files | What they turned out to be |
|---|---|---|---|
eval() | 5 | 5 | 3 in comments, 2 in a config parser inside a vendored library |
base64_decode() | 39 | 30 | Encoding of binary assets and API payloads |
gzinflate/gzuncompress | 33 | 7 | HTTP response decompression |
str_rot13() | 1 | 1 | Test fixture |
create_function() | 0 | 0 | Removed in PHP 8 |
preg_replace with /e | 2 | 2 | Both false positives: the regex matched the word “textarea” |
shell_exec/passthru/popen | 7 | 4 | Mail transport and media metadata extraction |
exec() | 5 | 4 | Same libraries |
curl_exec() | 11 | 7 | HTTP clients |
fsockopen() | 18 | 11 | SMTP transport |
move_uploaded_file() | 9 | 5 | Media handling |
unserialize() | 54 | 34 | Options and cache storage |
extract() | 21 | 15 | Template variable scoping |
| SQL concatenation | 382 | 89 | Mostly prepared statements matching a loose regex |
| Total | 588 | — | Zero genuine findings |
Now the combination patterns, on the same corpus:
| Pattern | Hits on 1.67 M lines |
|---|---|
eval(base64_decode(...)) | 0 |
eval on a superglobal | 0 |
exec on a superglobal | 0 |
That is the whole argument for layering. Single-function patterns generated 588 items across clean code. Combination patterns generated none. When a combination pattern fires, it means something, which is exactly the property a security check needs and the reason to run both rather than only the first.
The file-level heuristic
| Heuristic | Files flagged | Share of corpus |
|---|---|---|
| Contains a variable function call | 289 | 3.9% |
| Contains a decode function | 37 | 0.5% |
| Contains both, same file | 11 | 0.15% |
Eleven files out of 7,441 is a triage you can complete over a coffee. All eleven were benign on inspection: a hex2bin polyfill that defines the function rather than calling it suspiciously, a mailer, an HTTP client, an ID3 tag parser, and a helper class using callables. The value is not that it found something. It is that opening eleven files by hand is a task a human will actually do, whereas reviewing 588 grep hits is a task a human will abandon halfway.
Detection test: does any of it work?
A checklist that has never caught anything is unfalsifiable. We wrote four samples in an isolated directory outside any web root, scanned them, and deleted them. Three were textbook backdoor patterns; the fourth was legitimate code written to trigger the naive patterns.
| Sample | Technique | Naive patterns | Combinations | File heuristic |
|---|---|---|---|---|
| 1 | Function name built from base64, called as a variable function | partial | MISSED | caught |
| 2 | eval(gzinflate(base64_decode(...))) | caught | caught | caught |
| 3 | system($_GET['cmd']) | caught | caught | — |
| 4 | Benign code using base64_decode and curl_exec | false positive | correctly silent | correctly silent |
Sample 1 is the important row. It builds the string eval from base64 at runtime and invokes it as $f(...). The word eval never appears in the file. Every combination pattern in every version of this checklist we have seen published misses it, and so did ours. Only the file-level heuristic — decode function and dynamic call present in the same file — flagged it.
Draw the correct conclusion from that. A static scan raises your floor; it does not prove code is clean. An attacker who knows your patterns writes around them in four lines. Combine the scan with the runtime checks in section 4, the network capture in particular, because a backdoor that never contacts anything is not much of a backdoor.
Two checks that cost nothing
# PHP files inside upload directories. On a clean install this is zero;
# any result is either a webshell or a design decision you should question.
find /var/www/your-script/uploads /var/www/your-script/storage \
-name "*.php" 2>/dev/null
# World-writable directories. Also zero on a correctly deployed install.
find /var/www/your-script -type d -perm -o+w 2>/dev/null
Both returned zero on our corpus, which is what a correctly deployed install looks like. A non-zero result on either is worth investigating before anything else in this article, because both are post-compromise indicators rather than code-quality signals. For the deployment configuration that keeps them at zero, see our Ubuntu 24.04 server hardening walkthrough, and for how hosting choice affects your exposure, our Cloudways vs. Hostinger VPS comparison.
6. Triage: What to Do With a Hit
A hit inside a comment
wp-includes/class-snoopy.php:678: // I didn't use preg eval (//e) since that is
wp-includes/class-json.php:24: * JavaScript, and can be directly eval()'ed with
Three of our five eval hits were comments. Filter them before spending attention, while remembering that a stripped comment can also hide a payload on the line below:
# Strip comments, then re-scan what remains
find /var/www/your-script -name '*.php' ! -path '*/vendor/*' -print0 \
| xargs -0 -I{} php -w {} 2>/dev/null \
| grep -nE 'eval[[:space:]]*\(|base64_decode[[:space:]]*\('
php -w outputs the file with comments and whitespace removed. Anything surviving that filter is executable code.
A hit that is a false positive from your own regex
wp-includes/class-wp-editor.php:306:
$content = preg_replace( '%</textarea%i', '</textarea', $content );
Both of our preg_replace /e hits were this: the pattern looked for a lowercase e among the modifiers and found the e in “textarea” before the closing delimiter. The /e modifier was removed in PHP 7 and cannot appear in code running on a supported version at all. Delete this check from your copy of the script rather than triaging its output forever; a check that only produces false positives trains you to ignore output, which is worse than not running it.
A hit in a vendored library
Two of our eval hits were real calls inside a well-known HTML sanitiser, which uses eval to parse configuration values. Do not read the library. Compare it against a clean copy:
# Get the same version from the canonical source, into a scratch directory
mkdir -p /tmp/clean && cd /tmp/clean
composer require vendor/package:X.Y.Z --no-scripts --quiet
# Diff the shipped copy against upstream. Only differences matter.
diff -rq /tmp/clean/vendor/vendor/package \
/var/www/your-script/vendor/vendor/package
rm -rf /tmp/clean
An identical tree means the library is upstream code and the hit is upstream’s design decision, not the author’s. A file that differs is the entire finding, and it is where a repackaged script hides its payload precisely because nobody reads vendor directories.
A hit that is genuinely dynamic execution
Trace what reaches it. The question is never “does this file call eval” but “can a request control what eval receives”:
# The variable feeding the dangerous call
grep -n "eval" suspect.php
# Every assignment to that variable in the file
grep -nE '\$payload[[:space:]]*=' suspect.php
# Does any request input reach it, directly or through a helper?
grep -nE '\$_(GET|POST|REQUEST|COOKIE|SERVER)|file_get_contents\([[:space:]]*["'"'"']php://input' suspect.php
A constant that never leaves the file is a design choice you may dislike. A value derived from a request is remote code execution, and the correct response is to stop the deployment and contact the author, not to patch around it.
Nothing found, and you still are not certain
Correct instinct. Deploy the script with the assumption that it will eventually be compromised, and make that outcome survivable:
# 1. Its own PHP-FPM pool: a compromise cannot reach neighbouring apps
# (see our CodeCanyon deployment guide for the full pool configuration)
# 2. Database privileges scoped to its own schema, never *.*
mysql -e "GRANT ALL PRIVILEGES ON script_db.* TO 'script_user'@'localhost';"
# 3. No PHP execution in writable paths
# location ~* ^/(uploads|storage)/.*\.php$ { deny all; }
# 4. A file integrity baseline, so you can answer "what changed"
find /var/www/your-script -name '*.php' -type f -exec sha256sum {} + \
| sort -k2 > /root/script-baseline-$(date +%F).sha256
# Then, weekly, from cron:
find /var/www/your-script -name '*.php' -type f -exec sha256sum {} + \
| sort -k2 > /tmp/script-now.sha256
diff /root/script-baseline-*.sha256 /tmp/script-now.sha256 \
|| echo "PHP files changed since baseline - investigate"
The integrity baseline is the highest-value item on this page. A static audit tells you about the code you received once. A baseline tells you the day something modifies it, which is the actual question during an incident and the one nobody can answer after the fact.
If you find something real
The sequence matters, because the instinct to delete the offending file destroys the evidence you need to answer the only question that counts: did this ever run, and what did it reach.
# 1. Preserve before you change anything. Copy, do not move.
install -d -m 700 /root/incident-$(date +%F)
cp -a /var/www/your-script /root/incident-$(date +%F)/code
cp /var/log/nginx/*access.log /root/incident-$(date +%F)/
# 2. Establish whether it ever executed. If the file was never requested
# and the script was never installed, you found it in time.
grep -F "suspect-file.php" /var/log/nginx/*access.log*
zgrep -F "suspect-file.php" /var/log/nginx/*access.log.*.gz 2>/dev/null
# 3. Look for the outbound side. A backdoor that never contacted anything
# is a backdoor that never ran.
grep -rEn "curl_exec|fsockopen|file_get_contents\(\s*['\"]https?" \
/root/incident-$(date +%F)/code/suspect-file.php
# 4. Only then remove it, from a copy you have already preserved.
If the access log shows the file was requested, treat the server as compromised rather than the file as removable. Rotate every credential the machine held — database passwords, API keys in .env, SSH keys, and any token the application was configured with — because you cannot know which were read. Restore from a backup predating the earliest request, not from yesterday’s.
Report it to Envato with the item ID, the file path inside the archive and the pattern you matched. Their review process does catch items after publication, and a report with a reproducible finding is acted on. Request a refund through the same channel; a security finding is grounds for one.
If you found it in a nulled copy rather than a purchased one, there is nobody to report it to and nothing to refund. That asymmetry is the practical argument for the licence, independent of the legal one.
7. Frequently Asked Questions
Are CodeCanyon scripts safe to use in production?
Paid CodeCanyon items are generally safe from deliberate backdoors, but Envato does not audit for security. Its review verifies the item works and is documented; injection testing, CSRF coverage and dependency CVEs remain entirely yours. Run a static scan, test CSRF and uploads on staging, and isolate the script in its own PHP-FPM pool before it sees traffic.
How do I check a PHP script for backdoors?
Scan in three layers. Single-function greps build an inventory but are noisy: ours produced 588 hits on 1.67 million lines of clean code, all benign. Combination patterns such as eval(base64_decode(...)) returned zero on that same corpus. Then flag files containing both a decode function and a dynamic call, which reduced 7,441 files to 11.
Is it safe to use nulled CodeCanyon scripts?
No. Cracking, repackaging and distributing a script costs effort that is funded by what the distributor adds: an authenticated backdoor, an install beacon, or SEO links served only to crawlers. A licence costs $39 to $79. The cheapest possible incident response — restore, rotate every credential, audit what left the server — costs a full day.
Does grepping for eval and base64_decode actually find backdoors?
Only unobfuscated ones. Our four-line test sample built the string “eval” from base64 and called it as a variable function, so the word never appears in the file; every naive and combination pattern missed it. Only a file-level heuristic flagging decode plus dynamic call caught it. Static scanning raises your floor, it does not certify code.
What should I check before deploying a purchased PHP script?
Five things, in order: run the layered static scan; replay a state-changing POST without a CSRF token; upload a file whose declared type and content disagree and confirm it cannot execute; run composer audit and check no dev packages shipped; capture outbound traffic during install to see what the licence check actually transmits.
8. Final Audit Checklist
# --- Static, on the unpacked archive, before it reaches a web root ---
php-audit /srv/staging/your-script
php -l <each modified file> # syntax, catches mangled injections
find /srv/staging/your-script -name '*.php' ! -path '*/vendor/*' -print0 \
| xargs -0 -I{} php -w {} | grep -E 'eval[[:space:]]*\(' # comment-stripped rescan
# --- Provenance ---
composer audit
composer show --installed | grep -Ei "phpunit|faker|ignition" # expect: nothing
diff -rq /tmp/clean/vendor /srv/staging/your-script/vendor # vendor vs upstream
# --- Runtime, on staging only, never production ---
curl -i -X POST https://staging.example.com/account/email \
-H "Cookie: PHPSESSID=VALID" -d "email=attacker@example.com" # expect 403/419
# upload probe.gif.php, then confirm it does not execute
tcpdump -i any -n 'tcp port 443' -w /tmp/install.pcap # what phones home
# --- Deployment posture, before go-live ---
find /var/www/your-script -type d -perm -o+w # expect: nothing
find /var/www/your-script/uploads -name '*.php' # expect: nothing
mysql -e "SHOW GRANTS FOR 'script_user'@'localhost';" # scoped, not *.*
grep -E "listen = " /etc/php/8.3/fpm/pool.d/script.conf # its own pool
curl -s -o /dev/null -w "%{http_code}\n" https://your-site/.env # expect: 404
# --- Ongoing ---
find /var/www/your-script -name '*.php' -exec sha256sum {} + | sort -k2 \
> /root/script-baseline-$(date +%F).sha256 # then diff weekly
The line that will actually save you is the last one. A static audit describes the code you received on one day. A file integrity baseline, diffed weekly from cron, is the only item here that answers the question you will genuinely be asked during an incident: what changed, and when.
If you run this scanner against a script and it flags something you cannot classify, send us the pattern and the surrounding lines with the identifying details removed. We add real-world cases to this article as we see them.