Is eval() safe in WordPress plugins? In most WordPress development scenarios, the answer is that eval() should be avoided. The function executes a string as PHP code, which means that any attacker-controlled input that reaches it can potentially become executable server-side code.
The danger is not that eval() automatically creates a vulnerability every time it appears.
The danger is what it represents:
String data
↓
interpreted as PHP
↓
executed by the server
If an attacker can influence that string, the boundary between data and executable code disappears.
In a WordPress plugin, that can turn an ordinary input-validation mistake into something considerably more serious, including arbitrary PHP execution.
This guide explains how eval() works, why it is dangerous in WordPress plugins, whether sanitization makes it safe, why legitimate-looking use cases usually have better alternatives and what developers should look for when auditing existing plugin code.
What is eval() in PHP?
eval() is a PHP language construct that evaluates a string as PHP code.
A basic example is:
$code = 'echo "Hello";';
eval( $code );
PHP interprets the contents of $code as executable PHP.
Conceptually:
$code
=
ordinary string
eval( $code )
=
execute the string as PHP
This capability is powerful because PHP code can be generated dynamically at runtime.
It is dangerous for exactly the same reason.
Why eval() creates a dangerous security boundary
Most application data is supposed to remain data.
For example:
User enters:
Hello Jessica
Application stores:
Hello Jessica
Application displays:
Hello Jessica
With eval(), a string can instead cross into the execution layer:
Input
↓
string manipulation
↓
eval()
↓
PHP execution
If an attacker can control enough of that string, the application may execute instructions chosen by the attacker.
The central question is data flow
When auditing eval(), the important question is not simply:
Does this plugin contain eval()?
It is:
Where does the evaluated string come from?
For example:
eval( $code );
tells you very little by itself.
You need to trace $code backwards.
Possible sources include:
- hard-coded plugin code;
- WordPress options;
- post metadata;
- user metadata;
- form submissions;
- AJAX requests;
- REST API requests;
- uploaded files;
- remote API responses;
- database content;
- shortcode attributes;
- URL parameters;
- cookies;
- HTTP headers.
The closer attacker-controlled data gets to eval(), the more serious the design becomes.
A clearly unsafe eval() example
Consider:
$code = $_POST['code'];
eval( $code );
This creates a direct path:
HTTP request
↓
$_POST
↓
eval()
↓
PHP execution
If the request can reach this code, the application is effectively allowing request data to become executable PHP.
That is not ordinary input processing.
It is a code-execution mechanism.
Authentication alone does not make eval() safe
Suppose the previous example is restricted to logged-in administrators.
The risk is reduced compared with a public endpoint, but the underlying design remains highly sensitive.
Administrator-only execution can still become dangerous through:
- compromised administrator accounts;
- cross-site request forgery where request protection is missing;
- stored malicious values;
- another vulnerability that modifies the stored code;
- incorrect capability checks;
- future changes that broaden access.
Authentication should therefore not be treated as a reason to execute arbitrary PHP strings unnecessarily.
Capabilities and code execution are different concerns
A WordPress capability check can determine whether a user is authorized to perform an administrative operation.
For example:
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
The official WordPress documentation for current_user_can() explains how capabilities should be checked for the current user.
But this:
current_user_can( 'manage_options' )
+
eval( $arbitrary_code )
still creates a PHP execution feature.
The capability check controls who can reach it.
It does not make arbitrary code execution intrinsically safe.
For a deeper explanation of the permission model, see WordPress User Roles and Capabilities Explained.
Why sanitization does not solve the fundamental problem
A common idea is:
$code = sanitize_text_field( $_POST['code'] );
eval( $code );
This is not a sound way to make arbitrary PHP execution safe.
WordPress sanitization functions are designed for specific data types and contexts.
They are not general-purpose PHP code validators.
The official WordPress security documentation on sanitizing data describes sanitization as the process of cleaning input according to the expected data type.
Sanitization requires an expected data type
Suppose a setting should contain an integer.
You can constrain it accordingly:
$items = absint( $_POST['items'] );
If a field should contain an email address:
$email = sanitize_email( $_POST['email'] );
If a field should contain ordinary text:
$title = sanitize_text_field( $_POST['title'] );
But PHP code is not a narrow data type.
PHP itself contains:
- variables;
- function calls;
- objects;
- file operations;
- network operations;
- database operations;
- process interactions;
- dynamic function calls;
- language constructs.
Trying to sanitize arbitrary PHP into “safe PHP” is a fundamentally different problem from validating an integer or email address.
Escaping is not the answer either
Escaping protects output contexts.
For example:
echo esc_html( $value );
helps safely render text into HTML.
The official WordPress documentation on escaping data explains the principle of escaping output as late as possible.
But:
eval( esc_html( $code ) );
does not represent a meaningful security model.
HTML escaping and PHP execution are different contexts.
Validation is more useful than trying to clean arbitrary code
Secure WordPress development usually works best when an application defines exactly what input is acceptable.
For example:
$allowed_layouts = array(
'grid',
'list',
'compact',
);
$layout = isset( $_POST['layout'] )
? sanitize_key( wp_unslash( $_POST['layout'] ) )
: '';
if ( ! in_array( $layout, $allowed_layouts, true ) ) {
$layout = 'grid';
}
The application accepts a known set of values.
It does not accept arbitrary PHP capable of describing what should happen.
Use data to select behavior instead of executing code
This is one of the most useful replacements for eval().
Suppose a plugin wants administrators to choose an operation.
An unsafe design might store PHP:
$action_code = get_option( 'custom_action_code' );
eval( $action_code );
A safer design stores an identifier:
custom_action
=
clear_cache
Then maps that identifier to predefined behavior:
$actions = array(
'clear_cache' => 'example_clear_cache',
'sync_data' => 'example_sync_data',
);
$selected = get_option( 'custom_action' );
if ( isset( $actions[ $selected ] ) ) {
call_user_func( $actions[ $selected ] );
}
The important architectural difference is:
Unsafe:
data defines executable PHP
Safer:
data selects predefined behavior
Prefer explicit functions
Instead of:
eval( '$result = calculate_total( $order );' );
call the function directly:
$result = calculate_total( $order );
This improves:
- readability;
- static analysis;
- debugging;
- IDE support;
- security review;
- testing.
Prefer callbacks when behavior needs to be configurable
WordPress itself is heavily callback-driven.
Hooks allow plugins to register behavior without generating PHP strings.
For example:
add_action( 'example_process_order', 'example_process_order' );
function example_process_order( $order_id ) {
// Process order.
}
Then:
do_action( 'example_process_order', $order_id );
This provides runtime extensibility without converting arbitrary strings into PHP.
WordPress hooks are usually a better extension mechanism
If a plugin uses eval() because developers need customization points, consider:
- actions;
- filters;
- callbacks;
- interfaces;
- configuration arrays;
- registered handlers.
For the underlying lifecycle, see WordPress Hooks: plugins_loaded vs init.
Dynamic behavior does not require dynamic PHP source code
Suppose a plugin supports several processing strategies:
standard
aggressive
conservative
You do not need:
eval( $generated_strategy_code );
You can use a strategy map:
$strategies = array(
'standard' => 'example_standard_strategy',
'aggressive' => 'example_aggressive_strategy',
'conservative' => 'example_conservative_strategy',
);
if ( isset( $strategies[ $mode ] ) ) {
call_user_func( $strategies[ $mode ] );
}
Better still, use classes or explicitly registered callbacks when the architecture becomes more complex.
Why eval() makes static analysis harder
Static analysis tools inspect source code to understand what it can do.
For example:
delete_option( 'example' );
is visible directly in the source.
But:
$code = $part_a . $part_b . $part_c;
eval( $code );
requires the analyzer to determine the runtime value of several variables before it can know which PHP statements will execute.
This makes code harder to inspect for:
- security vulnerabilities;
- deprecated APIs;
- dead code;
- dependency relationships;
- unexpected side effects.
eval() also makes code review harder
A reviewer can understand:
update_option( 'example_mode', $mode );
immediately.
With generated code:
$code = build_runtime_code( $configuration );
eval( $code );
the reviewer must understand:
- where the configuration originated;
- how the string is generated;
- which characters are allowed;
- which PHP constructs can appear;
- whether stored data can alter execution;
- whether filters can modify the generated code.
The review surface expands significantly.
Why eval() complicates debugging
Dynamically evaluated code can make debugging more difficult because the executed source may not exist as ordinary source code in the plugin files.
That can complicate:
- stack traces;
- breakpoints;
- source inspection;
- error reproduction;
- code search.
Explicit functions and classes generally produce a clearer execution path.
Why eval() complicates maintenance
WordPress plugins can remain installed for years.
Code that initially appears clever can later be maintained by developers who did not design it.
A runtime code generator requires them to understand two programs:
the code that generates PHP
+
the PHP that gets generated
That additional abstraction is rarely justified for ordinary plugin functionality.
eval() and remote code execution
The most serious eval() scenarios are those where untrusted input can influence the evaluated string.
The data path may resemble:
Attacker input
↓
insufficient validation
↓
stored or transformed string
↓
eval()
↓
arbitrary PHP execution
If exploitation allows attacker-controlled PHP to execute on the server, the impact can extend far beyond one WordPress page.
What could arbitrary PHP execution affect?
PHP running inside WordPress may potentially interact with:
- the WordPress database;
- site files;
- plugin files;
- theme files;
- users;
- options;
- authentication data;
- remote services;
- server-accessible resources.
The exact impact depends on the server environment and process permissions.
This is why code execution vulnerabilities are treated differently from ordinary display bugs.
Stored input can be just as dangerous as direct input
Developers sometimes inspect code such as:
$code = get_option( 'example_code' );
eval( $code );
and conclude that it is safer because no $_POST variable appears near eval().
That conclusion is incomplete.
You must determine how example_code is written.
The actual path may be:
Admin form
↓
update_option()
↓
database
↓
get_option()
↓
eval()
The database does not automatically make data trusted.
Trace both sources and sinks
Security review often becomes clearer when you identify:
SOURCE
Where does the data come from?
TRANSFORMATIONS
What happens to it?
SINK
Where does it become dangerous?
For an eval() audit:
Source:
request / database / remote API
Transformation:
concatenation / decoding / filtering
Sink:
eval()
The complete path determines the actual risk.
Base64 encoding does not make eval() safer
Code such as:
eval( base64_decode( $value ) );
does not become safer because the PHP is encoded.
Base64 is an encoding format, not a security control.
The flow remains:
encoded string
↓
decode
↓
PHP string
↓
eval()
↓
execution
Obfuscated eval() deserves careful review
Suspicious code may attempt to hide execution through:
- Base64;
- string concatenation;
- compression;
- character codes;
- variable functions;
- nested decoding.
Obfuscation is not proof that code is malicious, but unexplained obfuscation around runtime execution deserves investigation.
Do not evaluate remote API responses
A particularly dangerous architecture is:
$response = wp_remote_get( $url );
$code = wp_remote_retrieve_body( $response );
eval( $code );
This effectively turns a remote server into a PHP code provider for the WordPress installation.
Even if the remote server is currently trusted, the security model now depends on:
- that server;
- its credentials;
- its infrastructure;
- its deployment process;
- the network interaction;
- the code-delivery mechanism.
Remote services should generally return data that local code interprets through a defined protocol, not PHP source code for execution.
Remote configuration should remain data
A safer remote response might be:
{
"feature_enabled": true,
"mode": "standard",
"batch_size": 25
}
The local plugin can validate those values and select predefined behavior.
This preserves the boundary:
Remote server
→ data
Plugin
→ code
REST API endpoints require authorization independently
If dynamic behavior can be configured through a REST endpoint, protect the endpoint using a proper permission_callback.
register_rest_route(
'example/v1',
'/settings',
array(
'methods' => 'POST',
'callback' => 'example_update_settings',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
)
);
The official WordPress REST API documentation for custom endpoints explains the role of permission callbacks.
For the broader security model, see WordPress REST API Security Basics.
AJAX endpoints require the same attention
An administrative JavaScript interface does not make its backend callback automatically safe.
An AJAX handler should independently verify authorization and request validity.
function example_ajax_save_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
array(
'message' => 'Permission denied.',
),
403
);
}
check_ajax_referer(
'example_save_settings',
'nonce'
);
// Process validated settings.
}
For WordPress AJAX fundamentals, see the official WordPress AJAX documentation.
Nonces do not make eval() safe either
A nonce can help protect an administrative request from certain request-forgery scenarios.
It does not transform arbitrary PHP into safe PHP.
The security layers answer different questions:
Capability
→ Is the user authorized?
Nonce
→ Is the request associated with the expected interaction?
Validation
→ Is the submitted value acceptable?
eval()
→ Execute this string as PHP
The first three controls do not remove the fundamental power of the fourth.
The official WordPress nonce documentation also makes clear that nonces should not be treated as authentication or authorization.
What about PHP snippets plugins?
Some WordPress products intentionally allow administrators to create executable snippets.
That is a different product requirement from an ordinary plugin accidentally relying on eval().
A snippets system is deliberately implementing code execution.
Its threat model therefore needs to account for:
- who can create snippets;
- who can edit snippets;
- who can activate snippets;
- how syntax errors are handled;
- how recovery works;
- how requests are protected;
- how code is stored;
- how execution is audited.
Intentional code execution is still code execution
If a product’s purpose is to let a trusted administrator execute PHP, then eliminating all code execution would eliminate the feature itself.
The security objective changes from:
Never execute configurable PHP
to:
Restrict a deliberately powerful feature
to the intended trusted principals
and provide strong operational safeguards.
This distinction should not be used to justify eval() in unrelated plugin features.
Prefer files or structured snippet execution architectures
Where executable administrator-defined code is genuinely required, a carefully designed architecture can provide better operational properties than scattering eval() calls through normal plugin logic.
Depending on the product, this may involve:
- dedicated snippet storage;
- strict capabilities;
- activation states;
- syntax validation;
- recovery modes;
- auditing;
- isolated execution paths.
The feature should be treated as privileged development functionality rather than ordinary configuration.
What about dynamic mathematical expressions?
Plugins sometimes use eval() to calculate formulas.
For example:
$formula = '( $price * 1.2 ) + 10';
eval( '$result = ' . $formula . ';' );
This is risky if the formula can be influenced by users.
If the product needs a formula language, use a parser that supports only the intended grammar.
For example:
Allowed:
numbers
+
-
*
/
(
)
Not allowed:
PHP functions
variables
objects
file operations
arbitrary statements
A restricted expression parser provides a much smaller execution language than PHP itself.
Do not build a PHP blacklist
A tempting approach is to reject obviously dangerous words:
exec
system
shell_exec
file_put_contents
and then evaluate everything else.
This is fragile.
PHP provides many ways to construct and invoke behavior dynamically, and future language or application changes can create bypasses.
A safer principle is:
Allow known-safe grammar
rather than
block known-dangerous strings
Allowlists are easier to reason about
Suppose a plugin accepts a sorting rule.
Instead of:
eval( $user_sorting_logic );
define:
$allowed = array(
'date',
'title',
'modified',
'menu_order',
);
Then reject anything outside that set.
This turns a programming-language problem into ordinary data validation.
What about create_function()?
Older PHP code sometimes used create_function() for dynamically generated functions.
That approach also relied on evaluated code and has been removed from modern PHP.
Modern WordPress plugins should use closures or ordinary functions instead.
For example:
$callback = function ( $value ) {
return strtoupper( $value );
};
No runtime PHP source generation is required.
Closures replace many old dynamic-code patterns
A plugin may need a callback configured at runtime.
Closures can capture required variables:
$prefix = 'Product: ';
$callback = function ( $title ) use ( $prefix ) {
return $prefix . $title;
};
This provides dynamic behavior while keeping the executable code visible in the plugin source.
Anonymous functions are still auditable code
Compare:
$callback = function ( $value ) {
return trim( $value );
};
with:
$code = '$value = trim( $value );';
eval( $code );
The first version is easier for:
- developers;
- static analyzers;
- security scanners;
- debuggers;
- code reviewers.
Use classes for configurable strategies
Complex dynamic behavior can often be represented using objects.
For example:
interface Example_Strategy {
public function process( $value );
}
class Example_Standard_Strategy implements Example_Strategy {
public function process( $value ) {
return $value;
}
}
The application can then select a registered strategy based on validated configuration.
No PHP source string needs to be generated.
Use WordPress filters for transformations
If third-party developers need to modify a value:
$value = apply_filters(
'example_processed_value',
$value,
$context
);
This is the extension model WordPress is built around.
Developers register real PHP callbacks in their own plugin or theme rather than storing PHP source inside application data.
Use WordPress actions for events
Likewise:
do_action(
'example_after_import',
$import_id
);
lets other code respond to an event without the original plugin evaluating arbitrary source strings.
eval() can hide dependency relationships
Consider:
$function = 'process_' . $mode;
$code = '$result = ' . $function . '( $data );';
eval( $code );
Even if $mode is controlled, this is unnecessarily indirect.
Use an explicit callback map:
$callbacks = array(
'standard' => 'process_standard',
'fast' => 'process_fast',
);
if ( isset( $callbacks[ $mode ] ) ) {
$result = call_user_func(
$callbacks[ $mode ],
$data
);
}
Better still, verify callbacks
If callback names are configurable, validate them against an explicit registry rather than accepting arbitrary callable names.
$callbacks = array(
'standard' => 'process_standard',
'fast' => 'process_fast',
);
if ( ! isset( $callbacks[ $mode ] ) ) {
return;
}
$callback = $callbacks[ $mode ];
if ( is_callable( $callback ) ) {
$result = call_user_func( $callback, $data );
}
This limits execution to handlers intentionally registered by the application.
Dynamic class names should also be constrained
Code such as:
$class = $_POST['handler'];
$handler = new $class();
does not use eval(), but it still illustrates why replacing eval() mechanically is not enough.
Dynamic execution mechanisms should be constrained to known application behavior.
Use an allowlisted class map:
$handlers = array(
'csv' => Example_CSV_Handler::class,
'json' => Example_JSON_Handler::class,
);
Avoid arbitrary function execution from request data
Replacing:
eval( $_POST['code'] );
with:
call_user_func( $_POST['function'] );
is not automatically secure.
The application still lets request data choose executable behavior.
The safer architecture is:
Request value
↓
validate against allowlist
↓
map to known callback
↓
execute predefined behavior
eval() in template systems
Historically, some template systems transformed template syntax into PHP and executed it dynamically.
Modern plugin architecture should prefer template engines and rendering approaches that clearly separate:
- template data;
- template syntax;
- application code.
If a plugin is effectively implementing its own PHP-like template language using eval(), the security model deserves careful review.
Do not evaluate shortcode content as PHP
Shortcodes already provide a structured WordPress extension mechanism.
For example:
[product_grid columns="3"]
The plugin can register:
add_shortcode(
'product_grid',
'example_product_grid_shortcode'
);
and validate the shortcode attributes.
There is normally no need to interpret shortcode content as arbitrary PHP.
Do not evaluate block attributes as PHP
Block attributes should represent structured data.
For example:
{
"columns": 3,
"layout": "grid",
"showTitle": true
}
The server-side rendering callback can validate those values and render known behavior.
Turning block attributes into PHP source defeats the separation between configuration and application code.
Database values should normally remain data
WordPress stores extensive configuration in the database.
This includes:
- options;
- post metadata;
- user metadata;
- term metadata;
- custom tables.
The database should not silently become a PHP source-code repository unless executable snippets are an explicit and carefully controlled product feature.
For database context, see The WordPress Database Structure, Explained.
Serialized data does not need eval()
PHP serialized data may look code-like:
a:2:{
s:4:"mode";
s:8:"standard";
s:5:"items";
i:20;
}
but it should not be evaluated as PHP.
WordPress provides serialization helpers for structured values.
The relationship between PHP serialization and migration safety is covered in Why Serialized Data Breaks Naive WordPress Migrations.
JSON should be decoded, not evaluated
If a remote API or configuration field contains JSON:
{
"mode": "standard",
"items": 20
}
decode it as data:
$data = json_decode( $json, true );
Then validate the resulting structure.
Do not transform structured data into PHP source simply to process it.
What about configuration expressions?
If users need conditions such as:
price > 100
country = IT
role = editor
build or use a restricted rule system.
For example:
Field:
price
Operator:
greater_than
Value:
100
The application then interprets a known data structure.
This is much easier to validate than arbitrary PHP:
return $price > 100;
Rules engines are safer abstractions for business logic
A structured rule might be stored as:
{
"field": "price",
"operator": "greater_than",
"value": 100
}
The application supports only registered operators:
$operators = array(
'equals',
'not_equals',
'greater_than',
'less_than',
);
This provides configurable behavior without exposing the complete PHP language.
What if eval() receives only hard-coded strings?
Consider:
eval( 'example_cleanup();' );
If the string is completely hard-coded and cannot be modified, the immediate injection risk is very different from evaluating user input.
But the code remains unnecessary.
This:
example_cleanup();
is simpler, easier to audit and easier to maintain.
Therefore, even when a particular eval() call is not exploitable, its presence can still indicate avoidable design complexity.
Not every eval() call has the same severity
Security reviews should avoid reducing analysis to:
eval found
=
site compromised
Instead, classify the data path.
For example:
Hard-coded constant string
→ low direct injection exposure
Trusted internal generated string
→ requires architecture review
Administrator-controlled stored string
→ privileged code-execution feature
Lower-privileged user input
→ severe risk
Unauthenticated request input
→ potentially critical risk
The actual impact depends on reachability, input control, authorization and execution context.
How to audit eval() in an existing WordPress plugin
Start by searching the plugin source for:
eval(
Then inspect every occurrence individually.
For each one, record:
- file;
- function or method;
- evaluated variable;
- source of that variable;
- transformations applied;
- required user capability;
- request protection;
- whether the code path is publicly reachable;
- whether
eval()is actually necessary.
Search for indirect runtime execution patterns too
A plugin audit should not stop at the literal word eval.
Depending on the threat model, review:
- dynamic callbacks;
- dynamic class construction;
- included files based on input;
- uploaded PHP files;
- shell execution;
- runtime code generation;
- unsafe deserialization patterns.
Different mechanisms create different risks, but the shared question is whether untrusted data can influence executable behavior.
File inclusion deserves separate attention
A plugin may avoid eval() but contain:
include $_GET['template'];
This is also dangerous because request data controls which file PHP executes.
File-path handling introduces its own risks, including directory traversal.
See Path Traversal: What It Is and How Plugins Prevent It.
Do not replace eval() with unsafe include()
A migration from:
eval( $code );
to:
include $user_selected_file;
does not automatically improve security.
If the file path is attacker-controlled, the application may simply have exchanged one dangerous execution primitive for another.
Use known file mappings rather than arbitrary paths.
Example of a safer template map
$templates = array(
'default' => plugin_dir_path( __FILE__ ) . 'templates/default.php',
'compact' => plugin_dir_path( __FILE__ ) . 'templates/compact.php',
);
$template = isset( $_GET['template'] )
? sanitize_key( wp_unslash( $_GET['template'] ) )
: 'default';
if ( ! isset( $templates[ $template ] ) ) {
$template = 'default';
}
include $templates[ $template ];
Request data selects a known identifier.
It does not provide an arbitrary filesystem path.
Use WordPress filesystem and path APIs appropriately
When plugins work with files, path handling should be deliberate.
Avoid concatenating untrusted input directly into:
include;require;- file deletion;
- file writing;
- archive extraction.
The same general principle applies:
Untrusted data
should not directly control
an execution-sensitive operation.
Plugin editors create another code-execution surface
WordPress installations may expose built-in file editing capabilities depending on configuration and permissions.
For hardened environments, reducing unnecessary code-editing surfaces can be useful.
WordPress supports disabling the built-in plugin and theme file editors through the DISALLOW_FILE_EDIT configuration constant:
define( 'DISALLOW_FILE_EDIT', true );
This does not make insecure plugin code safe, but it reduces one administrative code-editing surface and can be useful as part of a broader hardening strategy.
WordPress plugin security is layered
A secure plugin normally needs several complementary controls:
Authentication
↓
Authorization
↓
Request validation
↓
Input validation
↓
Safe processing
↓
Contextual output escaping
An eval() design can undermine that model by turning processed data back into executable instructions.
Capabilities should protect privileged configuration
If a plugin exposes high-impact configuration, use an appropriate capability.
Do not merely check:
is_user_logged_in()
when the operation requires administrative authority.
A logged-in Subscriber and an Administrator are both authenticated users, but they do not have the same permissions.
TheOneWP’s Role Manager can help define and maintain role capabilities when a site requires more granular administrative permissions.
Use nonces for state-changing admin requests
For administrative forms:
wp_nonce_field(
'example_save_settings',
'example_nonce'
);
and verify the request:
check_admin_referer(
'example_save_settings',
'example_nonce'
);
Then independently verify the capability required for the operation.
Sanitize according to expected input
Examples:
$title = sanitize_text_field( $raw_title );
$email = sanitize_email( $raw_email );
$url = esc_url_raw( $raw_url );
$count = absint( $raw_count );
$key = sanitize_key( $raw_key );
Each function reflects a data expectation.
This is fundamentally different from accepting a programming language as input.
Escape according to output context
For example:
echo esc_html( $text );
echo esc_attr( $attribute );
echo esc_url( $url );
Output escaping prevents one category of security problems.
It does not authorize actions or validate executable logic.
Prepare SQL safely
If a plugin builds database queries from variable input, use WordPress database APIs appropriately.
For example:
global $wpdb;
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
$post_id
);
The official wpdb::prepare() documentation explains placeholder-based query preparation.
This solves a different problem from eval(), but both illustrate the same broader rule: values should remain values rather than becoming executable syntax.
Do not confuse SQL injection with PHP code injection
These vulnerabilities target different interpreters.
SQL injection
→ untrusted data changes SQL instructions
PHP code injection
→ untrusted data changes PHP instructions
The defensive techniques therefore differ.
$wpdb->prepare() does not make eval() safe, just as PHP input sanitization does not replace SQL query preparation.
Security depends on the interpreter receiving the data
Whenever application data enters an interpreter, ask:
Which language is interpreting this value?
Examples:
Browser
→ HTML / JavaScript
Database
→ SQL
PHP runtime
→ PHP
Shell
→ shell commands
The required defense depends on the context.
Code injection can chain with other vulnerabilities
An eval() call may appear unreachable to ordinary users but become exploitable when combined with another weakness.
For example:
Weak permission check
+
stored option modification
+
eval( option )
=
code-execution path
This is why security audits trace data flow instead of reviewing individual lines in isolation.
Compromised administrator accounts change the threat model
WordPress administrators already possess substantial authority.
However, unnecessarily adding a custom PHP execution feature can increase what an attacker can do immediately after compromising such an account, particularly in environments where file editing or other administrative actions have been restricted.
Authentication hardening therefore remains relevant.
See A WordPress Login Hardening Checklist for the wider authentication layer.
Two-factor authentication can protect privileged accounts
TheOneWP’s Two-Factor Authentication can add another authentication factor for WordPress accounts.
This helps reduce account-compromise risk.
It does not make dangerous code-execution patterns acceptable, but layered security matters when administrative accounts can perform high-impact actions.
Limit login attempts as another authentication layer
Automated credential attacks are another route toward privileged-account compromise.
TheOneWP’s Access Manager includes access-control and login-protection capabilities that can form part of a broader WordPress authentication security strategy.
For the specific implementation concept, see How to Limit Login Attempts in WordPress.
Monitor authentication activity
Administrative code-execution risk becomes more serious if compromised credentials go unnoticed.
See How to Monitor WordPress Login Attempts for the authentication-monitoring side of a broader security program.
Backups remain essential before security refactoring
Removing dynamic execution from a mature plugin can change important behavior.
Before major refactoring or emergency remediation, create a recoverable backup.
TheOneWP’s Backup Manager can support complete, database-only or files-only recovery workflows depending on the situation.
For recovery verification, see How to Test a WordPress Backup Restore.
How to remove eval() from an existing plugin
Do not begin by deleting the function call blindly.
First determine what architectural problem it was solving.
A useful process is:
1. Locate eval()
2. Trace the evaluated value
3. Identify the intended behavior
4. Identify every supported input
5. Replace code strings with structured data
6. Map data to explicit handlers
7. Add validation
8. Add capability checks where required
9. Add request protection
10. Test every original behavior
11. Remove the eval() path
12. Run regression and security tests
Refactoring example: dynamic operation
Suppose the old implementation is:
$operation = get_option( 'example_operation' );
eval( $operation );
First identify the actual supported operations.
Suppose they are:
clear cache
rebuild index
sync records
Store identifiers instead:
clear_cache
rebuild_index
sync_records
Then map them:
$operations = array(
'clear_cache' => 'example_clear_cache',
'rebuild_index' => 'example_rebuild_index',
'sync_records' => 'example_sync_records',
);
$selected = get_option( 'example_operation' );
if ( isset( $operations[ $selected ] )
&& is_callable( $operations[ $selected ] ) ) {
call_user_func( $operations[ $selected ] );
}
Refactoring example: dynamic conditional logic
Suppose a plugin stores:
$price > 100 && $country === 'IT'
and evaluates it.
Replace it with structured rules:
array(
array(
'field' => 'price',
'operator' => 'greater_than',
'value' => 100,
),
array(
'field' => 'country',
'operator' => 'equals',
'value' => 'IT',
),
)
Then write explicit code that supports only recognized fields and operators.
Refactoring example: generated callback
Instead of:
$code = '
return function( $value ) {
return strtoupper( $value );
};
';
$callback = eval( $code );
use:
$callback = function ( $value ) {
return strtoupper( $value );
};
Refactoring example: configurable formatter
Suppose users can choose:
uppercase
lowercase
trim
Use:
$formatters = array(
'uppercase' => 'strtoupper',
'lowercase' => 'strtolower',
'trim' => 'trim',
);
if ( isset( $formatters[ $formatter ] ) ) {
$value = call_user_func(
$formatters[ $formatter ],
$value
);
}
If the formatter list is internal and fixed, configuration cannot invoke arbitrary PHP functions.
Refactoring example: templates
Instead of storing:
echo '<div>' . $title . '</div>';
as PHP code in the database, store:
template
=
card
and map it to a known template file or rendering callback.
Run automated tests after removing eval()
Dynamic code may hide assumptions that are not obvious during refactoring.
Test:
- normal input;
- empty input;
- invalid input;
- unexpected types;
- permission failures;
- AJAX requests;
- REST requests;
- stored configuration;
- upgrades from previous plugin versions.
Test migration of old configuration
If previous plugin versions stored PHP snippets or code-like expressions, replacing the execution architecture may require a data migration.
Do not assume old stored values automatically map to the new structured configuration.
Plan:
old configuration
↓
migration parser
↓
validated new structure
↓
new execution model
Keep the migration deterministic and test it against real historical data.
Do not silently execute legacy code during migration
A migration routine should not solve compatibility by evaluating old stored PHP merely to determine what it means.
If legacy values cannot be safely converted, require explicit administrator review rather than creating a hidden execution path inside the migration.
Audit database-stored code after removing eval()
Removing the runtime call does not automatically remove old code strings from the database.
Determine whether they should be:
- migrated;
- archived;
- deleted;
- retained as non-executable historical data.
Use appropriate database inspection rather than deleting unknown values blindly.
Use Database Manager for inspection
TheOneWP’s Database Manager can help inspect WordPress tables and stored values when auditing legacy plugin configuration.
This can be useful when tracing whether an evaluated value originated from:
wp_options;- post metadata;
- user metadata;
- plugin-specific tables.
Do not edit production data without recovery
Security remediation can itself cause outages if stored configuration is changed incorrectly.
Before bulk-removing or transforming legacy values:
- create a backup;
- test the migration on staging;
- verify the transformed configuration;
- test the affected plugin workflows.
A staging-first workflow is covered in Building a Staging-First WordPress Update Workflow.
How to review a third-party plugin containing eval()
If you discover eval() inside a third-party plugin, avoid jumping directly from source search to a conclusion about exploitability.
Investigate:
What data reaches eval()?
Who controls that data?
Which capability is required?
Can the code path be reached remotely?
Is the value stored?
Can another endpoint modify it?
Is a nonce relevant?
Is arbitrary PHP actually possible?
Why is eval() needed?
The answers determine whether you are looking at unnecessary legacy code, a deliberately privileged code feature or an exploitable injection path.
Plugin age is not evidence of safety
Code that has existed for years is not automatically secure.
Likewise, recently written code is not automatically insecure.
Review the actual implementation and data flow.
Long-lived WordPress installations benefit from periodic plugin audits. See Auditing WordPress Plugins on a Client Site.
Unused plugins still deserve attention
An installed plugin that is no longer operationally necessary creates maintenance overhead and may retain old code or data structures.
A periodic cleanup process can identify software that should be removed.
See A WordPress Plugin Cleanup Checklist.
Updates matter when security fixes are released
If a plugin vendor removes or hardens a dangerous execution path, leaving the vulnerable version installed preserves the old behavior.
For the broader maintenance risk, see The Risks of Not Updating WordPress Plugins.
Code execution should not depend on obscurity
Do not rely on:
- hidden admin pages;
- secret query parameters;
- undocumented AJAX actions;
- obfuscated JavaScript;
- unlinked URLs.
If reaching an endpoint would be dangerous when its URL becomes known, the endpoint needs proper authorization.
For the distinction between interface visibility and actual permissions, see Controlling WordPress Admin Page Visibility by Role.
Admin-only pages still require secure handlers
An administrative page may expose:
- settings;
- imports;
- exports;
- code snippets;
- database operations;
- filesystem actions.
Each sensitive action should verify authorization on the server.
Hiding the page from other roles is a user-interface decision, not the final security control.
What WordPress plugin developers should use instead of eval()
The correct replacement depends on the original requirement.
Need configurable actions?
→ allowlisted callbacks
Need extensibility?
→ actions and filters
Need templates?
→ template files or render callbacks
Need formulas?
→ restricted expression parser
Need business rules?
→ structured rules engine
Need configurable classes?
→ class registry
Need conditional behavior?
→ validated configuration
Need anonymous behavior?
→ closures
Need administrator PHP snippets?
→ deliberately designed privileged snippet system
When is eval() technically safe?
A completely fixed string that cannot be influenced externally can avoid the classic code-injection path.
For example:
eval( 'example_function();' );
But this provides no meaningful advantage over:
example_function();
So the practical question is often not:
Can I construct one eval() call
that is not exploitable?
It is:
Why does this WordPress plugin
need runtime PHP evaluation at all?
When might runtime code execution be an intentional requirement?
There are specialized products whose explicit purpose is to execute administrator-defined code.
Examples can include:
- developer snippet systems;
- development consoles;
- debugging environments;
- specialized automation systems.
In those cases, runtime code execution is part of the feature definition.
Such systems require a substantially stronger threat model and should not be used as a pattern for ordinary settings, forms, templates or plugin configuration.
A WordPress eval() security checklist
- Search the plugin for every
eval()call. - Trace the evaluated variable to its source.
- Determine whether request data can influence it.
- Determine whether database values can influence it.
- Determine who can modify those values.
- Determine whether remote API responses can influence it.
- Check capability requirements.
- Check nonce protection where applicable.
- Check AJAX endpoints.
- Check REST API endpoints.
- Check custom admin pages.
- Check import functionality.
- Check uploaded files.
- Check dynamic includes.
- Check dynamic callbacks.
- Check dynamic class construction.
- Check Base64 or other decoding around execution.
- Identify why runtime PHP generation exists.
- Replace code strings with structured data where possible.
- Use allowlisted callbacks.
- Use WordPress hooks for extensibility.
- Use closures for local dynamic behavior.
- Use restricted parsers for formulas.
- Use structured rules for business logic.
- Use known template mappings.
- Validate input according to expected type.
- Escape output according to context.
- Use appropriate capability checks.
- Protect state-changing requests with nonces.
- Test refactoring on staging.
- Back up before transforming legacy configuration.
- Audit old database values after migration.
- Run regression tests.
- Review the plugin again after the execution path is removed.
A practical decision tree
Does the plugin use eval()?
│
├── No
│ └── Continue normal security review
│
└── Yes
│
├── Is the evaluated string fixed?
│ ├── Yes
│ │ └── Replace eval() with direct code
│ │
│ └── No
│ │
│ ├── Can external data influence it?
│ │ ├── Yes
│ │ │ └── Treat as a high-priority security issue
│ │ │
│ │ └── No
│ │ └── Review why runtime generation exists
│ │
│ └── Is executable user code the product feature?
│ ├── No
│ │ └── Replace with structured behavior
│ │
│ └── Yes
│ └── Apply a privileged-code threat model
Example secure replacement architecture
Imagine a plugin currently stores PHP logic in an option:
example_processing_code
Replace it with:
example_processing_mode
Allowed values:
standard
fast
safe
Validate:
$allowed_modes = array(
'standard',
'fast',
'safe',
);
$mode = get_option(
'example_processing_mode',
'standard'
);
if ( ! in_array( $mode, $allowed_modes, true ) ) {
$mode = 'standard';
}
Map:
$handlers = array(
'standard' => 'example_process_standard',
'fast' => 'example_process_fast',
'safe' => 'example_process_safe',
);
Execute:
$callback = $handlers[ $mode ];
if ( is_callable( $callback ) ) {
call_user_func( $callback );
}
The database now contains configuration rather than executable PHP.
Why this architecture is easier to secure
The application can only execute:
example_process_standard()
example_process_fast()
example_process_safe()
because those are the only handlers registered in the map.
An attacker cannot simply submit:
some_other_function
and expect it to execute.
The input is constrained to known application states.
Why this architecture is easier to test
Each handler can have its own tests:
test_standard_processing()
test_fast_processing()
test_safe_processing()
The configuration validation can be tested independently.
This is substantially clearer than generating arbitrary source strings and attempting to test every possible runtime combination.
Why this architecture is easier to extend
If a new mode is required:
optimized
add:
'optimized' => 'example_process_optimized'
and implement the function.
The executable code remains part of the plugin source, where developers, version control and security tooling can inspect it.
Keep executable code in version-controlled source
For ordinary plugin functionality, PHP behavior should generally live in:
- plugin PHP files;
- classes;
- functions;
- registered callbacks;
- version-controlled source code.
Configuration belongs in:
- options;
- metadata;
- configuration arrays;
- structured database records.
Maintaining that distinction makes the system easier to reason about.
Use staging for security-sensitive refactoring
Replacing eval() can touch core plugin behavior.
A staging environment allows you to:
- migrate stored configuration;
- test old workflows;
- test invalid input;
- verify capabilities;
- inspect PHP logs;
- run regression tests.
See WordPress Staging Site Best Practices for a broader staging workflow.
Review logs after deployment
After replacing a dynamic execution system, monitor for:
- PHP errors;
- unexpected exceptions;
- failed AJAX actions;
- REST API errors;
- invalid configuration;
- permission failures.
This can reveal legacy assumptions that were not covered during testing.
Related guides
- Path Traversal: What It Is and How Plugins Prevent It
- WordPress REST API Security Basics
- Auditing WordPress Plugins on a Client Site
- A WordPress Plugin Cleanup Checklist
- A WordPress Login Hardening Checklist
Final recommendation
For ordinary WordPress plugin development, avoid eval().
The function turns a PHP string into executable code. If untrusted or insufficiently controlled data can influence that string, an input-handling weakness can become a server-side code-execution vulnerability.
Do not attempt to solve the problem by applying generic sanitization immediately before eval(). Sanitization works best when the application defines a narrow expected data type. Arbitrary PHP is a programming language, not a simple configuration value.
Instead, determine what the plugin is actually trying to make configurable. Store structured data and map that data to predefined behavior. Use callbacks, closures, classes, WordPress actions and filters, template mappings, restricted expression parsers or structured rule engines depending on the requirement.
If you discover eval() during a plugin audit, trace the complete data flow before assessing the specific risk. Determine where the evaluated value originates, who can modify it, how it is transformed and which authorization controls protect the execution path. A hard-coded eval() call and an unauthenticated user-controlled eval() call do not have the same security implications.
Use Role Manager and WordPress capabilities where privileged functionality needs carefully controlled access. Where reducing administrative code-editing surfaces is part of the site’s hardening strategy, WordPress can also disable the built-in plugin and theme editors through DISALLOW_FILE_EDIT. These controls complement secure plugin architecture; they do not compensate for arbitrary unsafe code execution.
Before refactoring legacy execution logic, create a recoverable backup with Backup Manager, test the change on staging and inspect stored legacy configuration with Database Manager where necessary.
The safest architectural principle is simple:
Configuration should describe
what the plugin should do.
It should not become
the PHP code that does it.

