Download the PHP package enshrined/wp-taint without Composer

On this page you can find all versions of the php package enshrined/wp-taint. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package wp-taint

wp-taint

Interprocedural taint analysis for WordPress PHP. Finds real XSS, SQL injection and broken authorization, with a source-to-sink trace on every finding.

explain is a command, not a scan flag, and --scope is not optional: without it the file is analysed alone, and anything whose taint arrives through an include or a hook comes back clean.

Documentation

Guide For
Getting started Install, first scan, reading a finding
Scanning a WordPress project Reporting on your code among thousands of third-party files
How it works Sources, sinks, taint kinds and the decisions behind them
CLI reference Every command, option, config key and rule
Troubleshooting A scan that looks stuck, wrong or too loud
Known limitations What the engine does at the edges, and why

Why this exists

Every existing option fails on WordPress in a specific way.

Tool Where it falls down on WP
Psalm taint analysis Precision depends on type inference. Plugin code is untyped array soup, so inference degrades and the taint graph degrades with it. Also needs a coherent autoloader, which themes do not have.
Semgrep / Opengrep Ignores types, which suits WP, but cross-function taint is limited to a single file. Interfile taint is roadmap, not shipped.
PHPStan No dataflow engine. Taint would have to be rebuilt on collectors, fighting per-file caching and parallelism.
WPCS sniffs Encode the right catalogue of escapers and sinks, but are token-based and intraprocedural, so they are noisy and miss anything crossing a function boundary.

wp-taint takes the SSA/CFG approach (which gives interprocedural taint cheaply) and the WPCS catalogue, which is the expensive, already-curated asset.

What it finds

Injection, via dataflow from source to sink:

Calls are followed through the value that names them: a callable in a variable, call_user_func(), array_map(), and hook dispatch. A callback registered with add_filter() that reads $_GET taints the filter's result at every apply_filters() site; one that escapes is credited.

Method calls resolve through the class hierarchy, so a method inherited from a parent or pulled in from a trait is followed to the body PHP would actually run, and parent:: dispatches past the override to the parent's.

Writes that go back through an argument are followed too, preg_match() into $matches, parse_str() into an array, a user function's &$out parameter, and the aliases created by $a = &$b and by-reference foreach.

And include/require join the two files' scopes, so the theme shape works: $title = $_GET['title']; include 'header.php'; connects to the template that echoes it. Paths fold from __DIR__, constants, and the pure path helpers WordPress builds them with. --no-follow-includes turns it off.

Broken authorization, via structural rules, which taint analysis structurally cannot find:

These walk the call graph rather than matching names, so a check delegated to a helper counts and a helper merely named like a check does not. A nonce is recorded as proving intent rather than entitlement, which is why it satisfies the AJAX rule and not the admin-post one.

Taint is never a boolean

esc_html() clears HTML taint and does nothing whatsoever for SQL. $wpdb->prepare() clears SQL and nothing else. esc_url_raw() is for redirects and database storage, not for HTML.

Every value carries a set of taint kinds, and every sanitizer clears a specific subset. That is the single most important modelling decision in the project, and it is why this reports the SQL injection in:

…while reporting nothing at all in:

Usage

Common options:

Option Effect
--format=json Full traces, for another tool or an agent
--fail-on=SEVERITY Exit 1 at or above this level. Default high.
--include-path=PATH Analyse for symbols, never report. Repeatable.
--exclude=GLOB Skip a tree. Repeatable.
--jobs=N Worker processes. Needs ext-pcntl.
--baseline=PATH Silence everything already known

Exit codes: 0 clean, 1 findings at or above --fail-on, 2 execution error, including any file that failed to parse. A file the scanner could not read is a file it could not clear, and a green build over an unread file is a lie.

Every command, option, config key and rule id is in the CLI reference.

Handing results to an agent

Use --format=json. Console output is lossy by design: colour codes, collapsed traces, truncated spans. The JSON carries the full trace, the taint kinds at every step, and the rule definition inline on each finding, so a reader coming to it cold needs no other context.

Why was this not flagged?

The failure mode a security scanner most needs to defend against is silence. explain turns "I don't trust it" into a specific, checkable statement:

The important case is the third one, where the engine says a path was abandoned, a call it could not resolve, rather than proved clean. --dynamic-calls=tainted then gives an upper bound on what might be missing, which is what you want when auditing the auditor. --dynamic-calls=clean is the other end: no assumptions, and a documented false negative wherever the engine lost the thread. See KNOWN_LIMITATIONS.md § dynamic calls.

Suppressing findings

Inline, with a mandatory reason:

A suppression without a reason is reported as an error of its own: "someone silenced this and nobody knows why" is a worse state than the original finding.

Or by baseline, for adopting the tool on an existing codebase:

Fingerprints deliberately exclude the line number, so a baseline survives unrelated edits above a finding. The suppressed count is always printed, so the debt stays visible.

The catalogue is data

Sources, sinks, sanitizers and propagators live in registries/*.toml. Adding a WordPress escaper never requires a code change.

Some plugins fire their hooks through wrappers of their own, which a scan rarely has the source for. registries/gravity-forms.toml models Gravity Forms' gf_do_action() and gf_apply_filters(). The WordPress catalogue extends it, so every scan loads it, and nothing needs turning on.

A PHP function the catalogue does not list behaves by what PHP declares it to return. registries/php-generated.toml is written from reflection by tools/generate-php-catalogue.php, and any hand-written entry wins over it.

A project-local wp-taint.toml in the scan root is loaded last and can add or override anything. Unknown keys are a hard error, not a warning: a typo in a security catalogue silently creates false negatives.

Stored sources, get_option(), get_post_meta() and friends, are on by default, because stored XSS is most of the WordPress CVE population. --no-stored-taint turns them off if you want to triage reflected issues first.

A value written to an option and read back from the same key connects, so a second-order flow with both ends in the scan (update_option( 'k', $_POST['x'] ) here, echo get_option( 'k' ) there) is followed the whole way rather than left at "an option might hold anything". Post and user meta stay unconnected per key.

Read the resolved catalogue with wp-taint registry:dump.

Design principles

  1. Deterministic. Same input, byte-identical output. Findings sorted by file, line, column, rule id. No randomness, no hash-order iteration, no timestamps in machine-readable output.
  2. Fail loudly, never silently. A file that fails to parse is a reported error, not a skipped file.
  3. Structure carries the value. The catalogue is data, not code.
  4. False positives are the product risk. A tool that cries wolf gets muted and then deleted. When in doubt, take the documented false negative.
  5. No LLM, no network, no telemetry on the analysis path. Fully deterministic and offline. The only thing that touches the network is tools/fetch-corpus.php, a developer tool.

Speed

A plugin is the unit: interprocedural taint crosses files, so the whole scan is parsed before analysis starts. Up to 4GB of parsed files stay in memory, and any file beyond that is parsed again when it is needed. --memory-budget changes the 4GB.

Plugin Lines Time Peak RSS
akismet 7,678 2s 73 MB
advanced-custom-fields 83,330 21s 429 MB
wordpress-seo 209,674 40s 602 MB
woocommerce 786,566 256s 2,373 MB

Single-threaded, on macOS 15 (arm64), PHP 8.3.32. Roughly 10 seconds per 50k lines. --jobs=4 roughly halves that (parsing stays serial, so it is not linear) and produces the same findings, which is enforced by a test rather than hoped for. A finding's trace can differ; see KNOWN_LIMITATIONS.md.

A first-party plugin or theme is a much smaller unit than any of these: a production theme of 926 files scans in 15 seconds.

Memory is the real constraint. bin/wp-taint raises the limit to 2 GB, which covers everything in the WordPress.org top fifty except WooCommerce; for a tree that size set WP_TAINT_MEMORY_LIMIT=4G. A site with many reference trees needs 8GB or more. See Scan a large site.

Scored against two suites written elsewhere

Two labelled fixture suites, neither written here, each with its own scorer and its own vocabulary for what a finding is called.

wp-taint-fixtures carries 109 annotations across 18 rule ids in Semgrep's ruleid: / ok: grammar, and reports precision and recall per rule. That framing matters: a tool that flags everything scores perfect recall and is useless.

default --no-unknown-provenance
Precision 0.98 1.00
Recall 0.94 0.77
F1 0.96 0.87

One false positive across 44 labelled-safe cases. --no-unknown-provenance trades eleven true positives for it, which is the wrong way round for a tool whose low band cannot fail a build.

ideas/wp-taint-analyser-fixtures pairs 36 scenarios as vulnerable and safe variants, cross-file and cross-plugin. Zero findings on all 36 safe variants, which is the strongest single result here: independent code, written correctly by someone else, and the tool says nothing about any of it.

They cover semantics the other benchmarks do not, context correctness, escape invalidation, weak sanitisers posing as real ones, where the corpus covers volume and the CVE set covers incidents.

Scored against real CVEs, both sides of the fix

47 published CVEs across 24 WordPress.org plugins, each pinned to the last vulnerable release and the release that fixed it. Scanning both is what makes it sharp: a finding that disappears when the bug is fixed is attributable in a way that a finding in a known-vulnerable plugin is not.

2 confirmed · 4 attributed · 20 reported · 20 silent, across the 46 pairs close enough to score (one CVE's vulnerable and fixed releases are five major versions apart, and a diff across that says nothing).

Four outcomes rather than two, because plenty of real fixes do not remove the flow. Confirmed means the findings vanish at the fix and the fixed release comes back clean; attributed means findings vanish while others remain, which a refactor in the fix release also produces. Reported is a finding on the vulnerable release that survives the fix, CVE-2022-2593 in Better Search Replace is SQL injection through unvalidated table names; we report 'DESCRIBE ' . $table in both releases, and the fix is array_map( 'trim', ... ). Scoring that a miss understates the engine as badly as scoring it a hit would flatter it, so it is counted apart and never added to the headline.

Where it fails, by class:

confirmed attributed reported silent
Code injection (CWE-94) 0 0 1 4
Deserialization (CWE-502) 1 0 0 2
Open redirect (CWE-601) 0 0 1 2
XSS (CWE-79) 0 2 7 3
Authorization (CWE-862/863) 0 2 5 5
Path traversal (CWE-22) 0 0 3 1
SQL injection (CWE-89) 1 0 2 2
SSRF (CWE-918) 0 0 1 1

Deserialization was a zero until this benchmark said so: stored data did not carry object-injection taint, on reasoning that holds for filesystem and URL sinks and not for this one. Fixing it caught CVE-2023-1196 in Advanced Custom Fields, now a confirmed hit, because the false positives that used to linger on the fixed release were themselves fixed and the release came back clean.

The CWE-94 cases are plugins whose purpose is executing admin-supplied code, so those CVEs are privilege-boundary bugs wearing a code-injection label. Two of the three open redirects still are not wp_redirect() flows the analysis can see; the third is now reported. Both are worth knowing before reading the column as a verdict on the rules.

"Attributed" is evidence, not proof. A finding can vanish because the fix refactored the line rather than because it was the bug. This is the sharpest measurement here and it is still not a confirmed hit.

Scored against an answer key we did not write

The fixture suite is ours, and 37 of its cases were written after the behaviour they test. The corpus is third-party code with no ground truth. Both flatter this tool in the same place.

The WordPress plugin review team published an intentionally vulnerable plugin in 2013 to teach plugin authors what their code does wrong, and a companion post enumerating every flaw in it. Somebody else's test, somebody else's answer key, written for a purpose unrelated to this tool.

It finds all 12. Three of them (the CSRF and control-flow bugs) the taint engine cannot see at all, and are caught by structural rules; filing them under "not a dataflow problem" would have been the easy way out.

The score is committed in tests/Fixtures/vulnerable-plugin-truth.json and checked in CI. It fails in both directions: a caught issue going quiet is a regression, and a missed one starting to fire means somebody fixed something without noticing and the file is now lying about where the tool stands.

How it compares

Against the same fixture suite, each fixture analysed on its own, with no project-specific tuning for anyone:

wp-taint Semgrep 1.174 Psalm 6.16
Vulnerable caught, all 184 92 / 92 67 / 92 38 / 92
Vulnerable caught, original 147 69 / 69 64 / 69 33 / 69
False positives, original 147 0 / 78 12 / 78 0 / 78

Quote the second row, not the first. 37 of these fixtures were added while fixing bugs the corpus exposed, and each pins a shape that work had just taught this engine to handle: dynamic calls, hook dispatch, include scope, by-reference writes. Semgrep gets 3 of 23 on them because they are a catalogue of what its analysis model does not attempt. On the suite that predates the work, it finds 93% of the vulnerable fixtures. It is a capable tool.

What separates them there is the safe half: 12 false positives against zero, clustered on values that were escaped by a route Semgrep cannot follow: an escaper applied through array_map(), an allowlist regex, a $wpdb table identifier that is not data.

Psalm is precise where it fires (zero false positives) and misses every SQL injection because nothing tells it what $wpdb is, which is a catalogue gap a plugin would close, and every authorization bug because those have no source and no sink, which taint analysis structurally cannot find.

Full working, including what the comparison does not prove and why the fixtures flatter us, in docs/benchmark.md.

Where it is imprecise

Honestly, in KNOWN_LIMITATIONS.md. Findings that crossed something the engine could not resolve carry imprecise: true so you can filter on it.

Development

The fixture suite in tests/Fixtures/ is the regression net: 145 vulnerable files and 149 safe ones that are superficially similar. The safe half matters more: a single false positive there fails the build.

Expectations are written as inline // wp-taint-expect <rule-id> <kind> annotations on the sink line and generated into .expected.json siblings by composer fixtures:build. CI fails if the two drift apart.

Requirements

PHP 8.2 or newer. No WordPress installation needed, the tool analyses source, it does not run it.

Licence

MIT.


All versions of wp-taint with dependencies

PHP Build Version
Package Version
Requires php Version >=8.2
ext-json Version *
ircmaxell/php-cfg Version ^0.8.1
laravel/prompts Version ^0.3.24
nikic/php-parser Version ^5.0
symfony/console Version ^7.0
yosymfony/toml Version ^1.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package enshrined/wp-taint contains the following files

Loading the files please wait ...