Dependency & supply-chain security

SkillSecurity

Use when a plugin or theme bundles a third-party PHP or JavaScript library, enqueues an asset from a CDN, fetches or executes code at runtime, manages dependencies with Composer, or prepares the distributable zip. Covers core-handle-first enqueuing, dependency vetting with composer audit, lockfile pinning, export-ignore artifact hygiene, Subresource Integrity for CDN assets via script_loader_tag, and refusal of eval() and remote include patterns. Prevents supply-chain compromise through stale, unvetted, or remotely loaded third-party code.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Dependency & supply-chain security skill

What this skill tells your AI

The instructions your AI receives, as published by wpultimatesecurity/wordpress-security-skills in skills/dependency-supply-chain-security/SKILL.md and read by ahel’s review.

When to use this skill

Use this skill whenever code consumes third-party code or assets:

  • Adding a Composer package, or vendoring a library copy into vendor/ or assets/lib/.
  • Enqueueing a script or stylesheet whose src is a full https:// CDN URL.
  • Loading PHP at runtime from the network, including "remote updater" patterns.
  • Preparing the distributable zip and deciding what ships inside vendor/.
  • Merging a snippet taken from Stack Overflow, a blog post, or an AI answer.

Do not reach for this skill for flaws in first-party code: use the output-escaping skill for XSS in your own markup, the capability-permission-checks skill for authorization, and the cron-background-job-security skill for cron callbacks.

A plugin is only as secure as the oldest library inside it. A large share of WordPress plugin CVEs are vulnerable bundled dependencies or compromised third-party code loaded at runtime, not bugs in the plugin's own logic.

Related: see the http-api-ssrf-prevention skill for validating URLs you fetch data from, the filesystem-security skill for confining local includes, and the secure-plugin-development skill for overall plugin hardening.

Core principles (and why they matter)

  1. Prefer core over bundling. WordPress registers jQuery, Underscore, Backbone, media libraries, and the block-editor React build under documented handles. wp_enqueue_script() the core handle instead of shipping your own copy: bundled duplicates drift out of date and conflict with other plugins and themes that use the core version. Core patches its bundled libraries; your vendored copy of the same library gets no patches.
  2. Vet before adding. Confirm the project shipped a release in the last ~12 months, scan its CVE history, and check the license is compatible with GPL where required. composer audit (Composer 2.4+) checks installed packages against the Packagist advisory database; the roave/security-advisories dev dependency refuses installs of known-vulnerable versions outright.
  3. Lock and pin. Commit composer.lock and build with composer install, never composer update. Avoid minimum-stability: dev and unbounded constraints like dev-main: they make builds unreproducible and unauditable, so a compromise upstream is indistinguishable from your own release.
  4. Ship a minimal artifact. Distributable zips must not contain dev dependencies, tests, CI configs, or demo data. .gitattributes export-ignore lines keep them out of git archive / composer archive builds, so attackers cannot read your tests to map untested code paths.
  5. Never load code from the network at runtime. eval(), string assert(), create_function() (removed in PHP 8), the preg /e modifier (removed in PHP 7), or include/require of a fetched file, including wp_remote_get() bodies and phar:// deserialization tricks, is RCE by design. Updates ship through an update channel; fetched payloads are not an update channel.
  6. Integrity-check front-end assets you do not host. Self-host when possible. If a CDN is required, pin an exact version URL and add Subresource Integrity (integrity + crossorigin attributes) via the script_loader_tag / style_loader_tag filters. Never enqueue a URL whose content can change, such as latest.js.
  7. Keep update latency low for what you ship. Do not ship code that blocks the auto-updates users rely on (auto_update_plugin, auto_update_theme, WP_AUTO_UPDATE_CORE, AUTOMATIC_UPDATER_DISABLED). Guard your own PHP and WordPress minimums with version_compare() and release patched builds fast when a bundled dependency announces a CVE.
  8. Treat pasted snippets as untrusted dependencies. Rewrite them against current WordPress APIs and check for removed functions (mysql_*, create_function()) before merging. A snippet copied from 2011 carries the threat model of 2011.

Step-by-step implementation

  1. Before vendoring, check whether WordPress core already registers the asset. The wp_register_script() documentation lists the default handles (jquery, jquery-ui-*, underscore, backbone, wp-api, and more). Enqueue the handle; do not print your own copy of the same file.

  2. Vet each new dependency before the first composer require:

    • Last tagged release within roughly the last 12 months.
    • No open high-severity advisories: run composer audit after adding.
    • License compatible with the plugin's distribution (GPL-compatible for WordPress.org hosting).
    • For non-Composer front-end libraries, check advisories with OWASP Dependency-Check or the vendor's security page.
  3. Configure Composer for reproducible builds and commit the lock file:

    {
        "name": "acme/my-plugin",
        "minimum-stability": "stable",
        "require": {
            "php": ">=7.4"
        },
        "require-dev": {
            "roave/security-advisories": "dev-latest"
        },
        "config": {
            "sort-packages": true
        }
    }
    

    Build with composer install --no-dev --optimize-autoloader. Only composer update intentionally bumps the lock file, and the diff gets reviewed like any other code change.

  4. Keep tests, CI configs, and demo data out of the shipped zip via .gitattributes:

    /.github            export-ignore
    /tests              export-ignore
    /demo               export-ignore
    /phpcs.xml.dist     export-ignore
    /phpunit.xml.dist   export-ignore
    
  5. Enforce your minimums at load time with version_compare() and refuse to run on stacks below them (see the reference file for the guard).

  6. For CDN assets: self-host if possible. Otherwise pin an exact version URL, store an integrity hash per asset, and add integrity + crossorigin through the script_loader_tag / style_loader_tag filters (full example in the reference file).

  7. Ship no auto-update blockers. Never distribute add_filter( 'auto_update_plugin', '__return_false' ); or a define( 'AUTOMATIC_UPDATER_DISABLED', true ); inside plugin code. If you must influence updates, filter only your own plugin:

    // Keep this plugin on the auto-update train; never silence the whole site.
    add_filter( 'auto_update_plugin', 'my_plugin_auto_update_self', 10, 2 );
    function my_plugin_auto_update_self( $update, $item ) {
        if ( isset( $item->slug ) && 'my-plugin' === $item->slug ) {
            return true;
        }
        return $update;
    }
    
  8. Grep the codebase before every release for runtime code loading: eval(, assert(, create_function(, preg_replace( with /e, include/require fed by wp_remote_get(), and phar:// stream usage.

  9. Rewrite pasted snippets against current APIs before merging. Anything calling mysql_* or create_function() was written for PHP that no longer runs your code.

Supporting references

ReferenceLoad when
Dependency & supply-chain cheatsheetChoosing the applicable WordPress API or control for dependency and supply-chain security.
Dependency & supply-chain checklistBefore final verification of the dependency & supply-chain controls.
Secure dependency managementImplementing version-gated dependency loading, core asset reuse, pinned CDN assets, and confined local includes.

Common AI mistakes / anti-patterns

Mistake 1 - Vendoring a stale copy of a library core already ships

// ❌ Insecure: bundles an old PHPMailer 5.x copy "because it worked".
// Old 5.x releases carry known remote-command-execution CVEs, and this copy
// never receives patches. Core's copy does.
require_once __DIR__ . '/vendor/phpmailer/phpmailer/PHPMailerAutoload.php';
// ✅ Secure: wp_mail() uses the PHPMailer version core ships and patches.
wp_mail( $to, $subject, $message, $headers );

Mistake 2 - CDN script with no version pin and no integrity

// ❌ Insecure: "chart.js" can change content at any time; the CDN operator
// (or anyone compromising it) controls the code that runs on every site.
wp_enqueue_script( 'my-charts', 'https://cdn.example.com/chart.js', array(), null );
// ✅ Secure: exact version URL + Subresource Integrity, or self-host the file.
wp_enqueue_script(
    'my-charts',
    'https://cdn.jsdelivr.net/npm/chart.js@4.1.2/dist/chart.umd.min.js',
    array(),
    '4.1.2', // Matches the version pinned in the URL path.
    true     // Load in the footer.
);
// integrity/crossorigin attributes are added in the script_loader_tag filter.

Mistake 3 - Runtime updater that fetches and executes code

// ❌ Insecure: RCE by design, and the plugin bricks itself when the host dies.
// $response = wp_remote_get( 'https://example.com/updater.php' );
// $body     = wp_remote_retrieve_body( $response );
// eval( $body );        // executes whatever the remote server returns
// include $tmp_path;    // same result if $tmp_path holds the fetched payload
// ✅ Secure: updates arrive through the WordPress.org update channel, or a
// signed self-hosted channel. Metadata travels over the network; code does not.
// Plugins on WordPress.org need no custom updater code at all.

Mistake 4 - Committing vendor/ with dev dependencies and CI configs

// ❌ Insecure: the distributable zip ships phpunit, phpcs configs, and test
// fixtures. Attackers read tests to map code paths nothing verifies.
// Built with: composer update (whatever was newest that Tuesday).
// ✅ Secure: export-ignore keeps dev trees out of archives; builds are pinned.
// .gitattributes:
//   /tests          export-ignore
//   /.github        export-ignore
// Build command:
//   composer install --no-dev --optimize-autoloader

Mistake 5 - minimum-stability: dev plus unbounded constraints

// ❌ Insecure: every build resolves differently; you cannot audit what ships.
{
    "minimum-stability": "dev",
    "require": { "monolog/monolog": "dev-main" }
}
// ✅ Secure: stable channel, bounded constraints, committed lock file.
{
    "minimum-stability": "stable",
    "require": { "monolog/monolog": "^3.0" }
}

Mistake 6 - Shipping removed dynamic-code constructs

// ❌ Insecure: fatals on PHP 8 (create_function removed) and on PHP 7 (/e
// modifier removed). Both execute strings as code, which is the point.
// $double  = create_function( '$a', 'return $a * 2;' );
// $bold    = preg_replace( '/<b>(.*?)<\/b>/e', 'strtoupper("$1")', $html );
// ✅ Secure: closures and callbacks; no string-as-code anywhere.
$double = function ( $a ) {
    return $a * 2;
};
$bold = preg_replace_callback(
    '/<b>(.*?)<\/b>/',
    function ( $m ) {
        return strtoupper( $m[1] );
    },
    $html
);

Correct code examples

A complete reference module covering core-handle-first enqueuing, pinned CDN assets with Subresource Integrity via the script_loader_tag filter, a version_compare() minimum-version gate, and a guarded local include confined to the plugin directory is in references/secure-dependency-management.php.

A before-adding / before-shipping checklist is in references/checklist.md, and a situation-to-practice lookup table is in references/cheatsheet.md.

Checklist

  • Core handles are used for assets WordPress registers (jquery, underscore, editor builds).
  • Every bundled dependency is maintained (release within ~12 months) and audited (composer audit).
  • roave/security-advisories is in require-dev for Composer projects.
  • composer.lock is committed; builds run composer install --no-dev.
  • minimum-stability is stable; no unbounded dev-* constraints.
  • .gitattributes export-ignore keeps tests, CI configs, and demo data out of the zip.
  • No eval(), string assert(), create_function(), preg /e, or remote include/require anywhere in shipped code.
  • CDN assets are self-hosted, or pinned to an exact version with integrity + crossorigin attributes.
  • No enqueued URL resolves to changeable content such as latest.js.
  • Shipped code contains no auto_update_plugin / auto_update_theme / AUTOMATIC_UPDATER_DISABLED blockers.
  • PHP and WordPress minimums are enforced with version_compare() before the plugin loads.
  • Pasted snippets were rewritten against current APIs before merging.

Official references

Signals

GitHub stars
31
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
dependency-supply-chain-security
Source
github.com/wpultimatesecurity/wordpress-security-skills