<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: WPlend.net</title>
    <description>The latest articles on DEV Community by WPlend.net (@wplend_net).</description>
    <link>https://dev.to/wplend_net</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4179212%2Fe1a717ab-02c7-4f72-b61d-ce9c94fa9473.png</url>
      <title>DEV Community: WPlend.net</title>
      <link>https://dev.to/wplend_net</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wplend_net"/>
    <language>en</language>
    <item>
      <title>How to check a WordPress plugin ZIP before installing</title>
      <dc:creator>WPlend.net</dc:creator>
      <pubDate>Mon, 12 Oct 2026 11:39:53 +0000</pubDate>
      <link>https://dev.to/wplend_net/how-to-check-a-wordpress-plugin-zip-before-installing-47md</link>
      <guid>https://dev.to/wplend_net/how-to-check-a-wordpress-plugin-zip-before-installing-47md</guid>
      <description>&lt;p&gt;A plugin ZIP reaches you in many ways: a client sends it by email, you find it in an old backup, you download it from a marketplace or a GPL redistributor, a freelancer leaves it in a shared folder. Once you click &lt;strong&gt;Upload Plugin&lt;/strong&gt;, every PHP file in that archive runs with full access to the site and its database.&lt;/p&gt;

&lt;p&gt;Here is the routine I use before installing any archive that did not come straight from wordpress.org. It takes about ten minutes and only needs a terminal.&lt;/p&gt;

&lt;p&gt;All examples use Linux/macOS. On Windows, run them in WSL.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Compare the checksum
&lt;/h2&gt;

&lt;p&gt;If the source publishes a SHA-256 checksum, check it first. It tells you the file you have is exactly the file they published — not truncated, not re-packed, not swapped on the way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sha256sum &lt;/span&gt;plugin.zip            &lt;span class="c"&gt;# Linux&lt;/span&gt;
shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256 plugin.zip        &lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SHA256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plugin.zip&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output must match the published value character for character. If it does not, stop here.&lt;/p&gt;

&lt;p&gt;A matching checksum proves file identity, not safety. It only means you have the same file the source has.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Look inside without installing
&lt;/h2&gt;

&lt;p&gt;List the contents before unpacking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip &lt;span class="nt"&gt;-l&lt;/span&gt; plugin.zip | less
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One top-level folder&lt;/strong&gt; named like the plugin (&lt;code&gt;my-plugin/&lt;/code&gt;). Several unrelated folders, or files at the root, are worth a second look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paths with &lt;code&gt;../&lt;/code&gt; or starting with &lt;code&gt;/&lt;/code&gt;.&lt;/strong&gt; A normal plugin never has them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unexpected file types&lt;/strong&gt;: &lt;code&gt;.exe&lt;/code&gt;, &lt;code&gt;.sh&lt;/code&gt;, &lt;code&gt;.phar&lt;/code&gt;, &lt;code&gt;.so&lt;/code&gt;, or PHP files with double extensions like &lt;code&gt;image.png.php&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now unpack it into a throwaway folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/zipcheck &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; unzip &lt;span class="nt"&gt;-q&lt;/span&gt; plugin.zip &lt;span class="nt"&gt;-d&lt;/span&gt; /tmp/zipcheck
&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp/zipcheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few quick searches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PHP files where there should only be assets&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-iname&lt;/span&gt; &lt;span class="s2"&gt;"*.php"&lt;/span&gt; &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*images*"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*img*"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*fonts*"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*css*"&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt;

&lt;span class="c"&gt;# hidden files and .htaccess&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;".*"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f

&lt;span class="c"&gt;# patterns common in injected code&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"eval&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|gzinflate&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|str_rot13&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|shell_exec&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;|base64_decode&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.php &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last search will have false positives — plenty of legitimate plugins use &lt;code&gt;base64_decode&lt;/code&gt; for icons or API payloads. What you are looking for is the classic shape of injected code: a long unreadable string fed into &lt;code&gt;eval(base64_decode(...))&lt;/code&gt; or &lt;code&gt;eval(gzinflate(...))&lt;/code&gt;, often on the first line of a file or appended after the closing &lt;code&gt;?&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Read the headers
&lt;/h2&gt;

&lt;p&gt;Every plugin has a main PHP file with a header comment; every theme has one in &lt;code&gt;style.css&lt;/code&gt;. Plugins usually repeat the requirements in &lt;code&gt;readme.txt&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rhE&lt;/span&gt; &lt;span class="s2"&gt;"^[ &lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;/*#@]*(Plugin Name|Theme Name|Version|Requires at least|Tested up to|Requires PHP|WC tested up to):"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.php &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;style.css &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;readme.txt &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version&lt;/strong&gt; is the one you expected. Mislabeled archives are common — you think you are installing 3.2 and it is actually 2.9.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requires PHP&lt;/strong&gt; is not higher than what your server runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tested up to&lt;/strong&gt; is not years behind your WordPress version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These values are declared by the developer. They are not a test result, but a mismatch is a good reason to ask questions before installing.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Run a PHP syntax check
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;php -l&lt;/code&gt; parses a file without running it. It catches truncated downloads, broken re-packs and files that were edited by hand and saved with an error — the kind of thing that gives you a white screen right after activation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.php"&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt; php &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"^No syntax errors"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No output means every file parsed. Run it with the same PHP major version as your server, since newer syntax fails on older PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Scan with ClamAV
&lt;/h2&gt;

&lt;p&gt;ClamAV is a free, open-source antivirus engine with signatures for common PHP web shells and injected code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian / Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;clamav
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop clamav-freshclam   &lt;span class="c"&gt;# if the service is running&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;freshclam                         &lt;span class="c"&gt;# update signatures&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start clamav-freshclam

&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;clamav   &lt;span class="c"&gt;# then follow brew's notes to create freshclam.conf&lt;/span&gt;
freshclam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clamscan &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; /tmp/zipcheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-i&lt;/code&gt; prints only infected files. Look at the summary at the end: &lt;code&gt;Infected files: 0&lt;/code&gt; is what you want.&lt;/p&gt;

&lt;p&gt;A clean result means no &lt;strong&gt;known&lt;/strong&gt; signature matched. It is not a code audit, and it will not catch a brand-new or hand-written backdoor — that is why steps 2 and 4 are still worth doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Read the changelog
&lt;/h2&gt;

&lt;p&gt;Find the entry for this version in &lt;code&gt;readme.txt&lt;/code&gt; (&lt;code&gt;== Changelog ==&lt;/code&gt; section), &lt;code&gt;changelog.txt&lt;/code&gt; or &lt;code&gt;CHANGELOG.md&lt;/code&gt;. It tells you whether you are about to apply a security fix, a major rewrite, or a release that changes database tables — which decides whether you need a backup and a maintenance window.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Install on staging first
&lt;/h2&gt;

&lt;p&gt;Everything above is static. None of it shows how the plugin behaves with your theme, your other plugins and your PHP settings. Install on a staging copy, enable &lt;code&gt;WP_DEBUG_LOG&lt;/code&gt;, click through the pages that matter and read &lt;code&gt;wp-content/debug.log&lt;/code&gt; before you touch production.&lt;/p&gt;

&lt;p&gt;Clean up when you are done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/zipcheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Doing it in one command
&lt;/h2&gt;

&lt;p&gt;Steps 1–6 are the same every time, so I put them into a single PHP script: &lt;a href="https://github.com/wplend/wp-zip-check" rel="noopener noreferrer"&gt;wp-zip-check&lt;/a&gt;. It calculates checksums, unpacks safely (skipping &lt;code&gt;../&lt;/code&gt; and absolute paths), counts files by type, reads the headers, compares the version, runs &lt;code&gt;php -l&lt;/code&gt; on every file, scans with ClamAV and prints the changelog entry for that version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://raw.githubusercontent.com/wplend/wp-zip-check/main/wp-zip-check.php
php wp-zip-check.php plugin.zip &lt;span class="nt"&gt;--expect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits with &lt;code&gt;0&lt;/code&gt; when all checks pass and &lt;code&gt;1&lt;/code&gt; when one fails, so you can run it over a folder of archives or in CI. &lt;code&gt;--json&lt;/code&gt; gives a machine-readable report. It is GPL-licensed, one file, no dependencies besides &lt;code&gt;php-zip&lt;/code&gt; and ClamAV.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] SHA-256 matches the published value&lt;/li&gt;
&lt;li&gt;[ ] One top-level folder, no &lt;code&gt;../&lt;/code&gt;, no unexpected file types&lt;/li&gt;
&lt;li&gt;[ ] No PHP in asset folders, no &lt;code&gt;eval(base64_decode(...))&lt;/code&gt; blobs&lt;/li&gt;
&lt;li&gt;[ ] Version, Requires PHP and Tested up to make sense&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;php -l&lt;/code&gt; passes on every file&lt;/li&gt;
&lt;li&gt;[ ] ClamAV: 0 infected files with fresh signatures&lt;/li&gt;
&lt;li&gt;[ ] Changelog read, backup taken&lt;/li&gt;
&lt;li&gt;[ ] Tested on staging&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I work on &lt;a href="https://wplend.net" rel="noopener noreferrer"&gt;WPlend&lt;/a&gt;, where every plugin and theme archive goes through these same automated checks after upload. wp-zip-check is the standalone version of that scanner.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
