Most WordPress sites get hacked through plugins. Not the core. Not the hosting. Plugins.
Here’s what happens: a business needs custom functionality, hires a developer or agency, gets a working plugin, and assumes it’s secure because it works. It launches. Three months later, the site’s serving malware or leaking customer data. The plugin worked perfectly — it just had holes a security researcher could spot in fifteen minutes.
At Webcomp Digitex, we’ve audited enough custom plugins to know that most security issues aren’t exotic. They’re basic input validation failures, hardcoded credentials, or developers trusting user input because “only admins will use this form.” That assumption breaks the moment someone finds the endpoint.
WordPress plugin security isn’t about adding a security plugin after the fact. It’s about writing code that assumes every input is hostile, every database query is vulnerable, and every file operation is a potential attack vector. That mindset separates plugins that survive in production from plugins that become liability risks.

Why Custom Plugins Get Targeted More Than You Think
Small sites don’t get hacked because hackers care about them specifically. They get hacked because automated bots scan for known vulnerabilities across millions of WordPress installs daily.
Custom plugins are valuable targets for a different reason: they’re not in the public repository, so they don’t get scrutinized by the WordPress security team or third-party auditors. A vulnerability in your custom booking plugin or membership system affects only your site — but that’s enough. One cross-site scripting flaw can compromise admin accounts. One SQL injection can dump your entire database.
We worked with a real estate client last year whose custom lead capture plugin had been live for eighteen months. Worked great. Generated thousands of leads. Then their hosting provider flagged suspicious database queries. Turned out the plugin’s search function wasn’t sanitizing inputs. Someone had found it and was extracting data quietly for weeks before getting sloppy enough to trigger rate limits.
The plugin was never designed to be malicious. The developer just didn’t validate inputs because “it’s only used on one form.” That’s how it starts.
The Three Vulnerabilities That Account for Most Plugin Exploits
Cross-site scripting. SQL injection. Remote code execution. These aren’t the only vulnerabilities, but they’re the ones that show up most often in real-world exploits.
Cross-site scripting (XSS) happens when your plugin outputs user-supplied data without escaping it. An attacker submits a comment or form entry containing JavaScript. Your plugin saves it and displays it back on the page. The script runs. Now the attacker can steal session cookies, redirect users, or modify page content. This is what happens when you trust input from forms, URL parameters, or POST data.
The fix isn’t complicated. Escape output everywhere. WordPress provides functions for this — `esc_html()`, `esc_url()`, `esc_attr()` — and they work. The problem is developers forget to use them consistently, especially in AJAX handlers or custom admin pages where “only trusted users will see this.”
SQL injection is worse. It happens when you build database queries by concatenating user input directly into SQL strings. An attacker crafts input that closes your query and injects their own. Now they can read, modify, or delete anything in your database.
WordPress has a solution here too: the `$wpdb->prepare()` method. It handles escaping and parameterization automatically. But we still see custom plugins building raw queries because it’s “faster” or because the developer didn’t know better. It’s not faster when your client’s site gets breached and their customer list ends up on a data broker site.
Remote code execution (RCE) is the nightmare scenario. It means an attacker can run arbitrary code on your server. This usually happens through unsafe file uploads, insecure deserialization, or plugins that evaluate user input as code. If someone can upload a PHP file disguised as an image, or if your plugin uses `eval()` on POST data, you’ve handed them the keys.
These three aren’t theoretical. They’re what researchers look for first when auditing plugins, and they’re what automated scanners test for when probing WordPress sites at scale.
How to Validate and Sanitize Every Input Point
Every form field. Every URL parameter. Every AJAX request. Every cookie. Treat all of it as malicious until you’ve validated and sanitized it.
Validation means checking that input matches what you expect. If a field should contain an email, verify it’s actually an email format. If it’s a number, make sure it’s numeric. If it’s a selection from a dropdown, confirm the submitted value is one of the allowed options. WordPress provides `is_email()`, `absint()`, and similar helpers. Use them before you do anything else with the data.
Sanitization means cleaning input to remove anything dangerous. WordPress provides sanitization functions for every common data type: `sanitize_text_field()` for plain text, `sanitize_email()` for email addresses, `sanitize_url()` for URLs. These strip out HTML, JavaScript, and SQL that doesn’t belong there.
Here’s what happens when you skip this: we audited a custom events plugin where the developer saved user-submitted event descriptions directly to the database without sanitization. An attacker submitted an event with a script tag in the description. When the event list page loaded, the script ran in every visitor’s browser. The site owner had no idea until Google flagged the domain for serving malware.
The fix took ten minutes. Add `sanitize_textarea_field()` before saving. Escape with `esc_html()` before output. That’s it. But because it wasn’t there from the start, the site spent two weeks delisted from search results and lost traffic they never fully recovered.
Validation and sanitization aren’t optional steps you add if there’s time. They’re the baseline. Every input point in your plugin should have both before you even think about saving or processing data.
Use Prepared Statements for Every Database Query
Raw SQL queries are the fastest way to create an SQL injection vulnerability. If you’re concatenating variables into query strings, you’re doing it wrong.
WordPress provides `$wpdb->prepare()` specifically to prevent this. It uses parameterized queries, which means user input is never interpreted as SQL code. You write placeholders in your query — `%s` for strings, `%d` for integers, `%f` for floats — and pass the values separately. WordPress handles escaping and quoting automatically.
It looks like this in practice:
“`
$user_id = absint( $_POST[‘user_id’] );
$wpdb->get_results( $wpdb->prepare( “SELECT * FROM wp_custom_table WHERE user_id = %d”, $user_id ) );
“`
That’s a safe query. The user input is validated as an integer, then passed through prepare(). Even if an attacker submits malicious input, it gets treated as data, not executable SQL.
Compare that to this:
“`
$user_id = $_POST[‘user_id’];
$wpdb->get_results( “SELECT * FROM wp_custom_table WHERE user_id = $user_id” );
“`
That’s vulnerable. If someone submits `1 OR 1=1`, they get every row. If they submit `1; DROP TABLE wp_custom_table`, you lose data. If they’re skilled enough, they extract your entire database.
We’ve seen production plugins with raw queries in user-facing features. Search filters. Data export functions. Member directories. All vulnerable. One had been live on a client site for two years before we caught it during a routine audit. Nobody had exploited it yet, but it was exploitable. That’s the risk — not that something has been hacked, but that it can be.
Every query in your plugin should use `$wpdb->prepare()`. No exceptions. Not even for “trusted” admin actions, because admin accounts get compromised too.
Implement Nonces to Prevent Cross-Site Request Forgery
Cross-site request forgery (CSRF) is when an attacker tricks a logged-in user into executing an action they didn’t intend to perform. They send a crafted link or embed a form on another site. When the victim clicks it while logged into WordPress, the action executes with their permissions.
This happens because browsers send cookies automatically with every request. If a user is logged into WordPress and clicks a malicious link that points to `yoursite.com/wp-admin/admin-ajax.php?action=delete_user&user_id=5`, the request includes their session cookie. WordPress sees a valid logged-in user and processes the action.
WordPress solves this with nonces — unique tokens that verify the request came from your site. You generate a nonce when rendering a form or link, then verify it when processing the request. If the nonce doesn’t match or has expired, you reject the request.
Creating a nonce:
“`
wp_nonce_field( ‘delete_user_action’, ‘delete_user_nonce’ );
“`
Verifying a nonce:
“`
if ( ! isset( $_POST[‘delete_user_nonce’] ) || ! wp_verify_nonce( $_POST[‘delete_user_nonce’], ‘delete_user_action’ ) ) {
wp_die( ‘Security check failed’ );
}
“`
That’s it. Every state-changing action in your plugin — saving settings, deleting records, processing payments — needs nonce verification. It’s two extra lines of code per form, and it prevents an entire class of exploits.
We rebuilt a membership plugin for a client last year that had no nonce checks anywhere. Any logged-in user could craft a URL to change plugin settings, delete members, or issue refunds just by getting someone to click a link. The original developer argued it wasn’t a real risk because “you’d have to know the URL structure.” That’s not security. That’s hoping nobody figures it out.
Nonces aren’t optional. They’re required for any action that changes data or state.
Escape Output Everywhere to Block Cross-Site Scripting
Sanitizing input prevents bad data from being saved. Escaping output prevents bad data from executing when it’s displayed.
Even if you sanitize perfectly on input, you still escape on output. Why? Because data can come from other sources. Third-party APIs. Other plugins. User-generated content from years ago before you added sanitization. If you’re displaying it, you escape it.
WordPress provides escaping functions for every context:
- `esc_html()` for HTML content
- `esc_attr()` for HTML attributes
- `esc_url()` for URLs
- `esc_js()` for inline JavaScript
- `esc_textarea()` for textarea fields
Use the right function for the context. Escaping HTML content with `esc_html()` won’t protect you if you’re outputting into a JavaScript string. The attacker adapts the payload to the context.
We audited a custom dashboard plugin that displayed user names in an admin table. The developer sanitized input correctly, but didn’t escape output because “it’s already clean.” Then an old account from before the sanitization was added still had a script tag in the name field. When an admin viewed the user table, the script ran with admin privileges. One oversight, one old record, full compromise.
Escape everything. Even if you think it’s safe. Even if it came from a “trusted” source. Even if it’s only visible to admins. Defense in depth means you don’t rely on one layer.
Restrict File Upload Types and Validate File Contents
File uploads are a common entry point for remote code execution attacks. An attacker uploads a PHP file disguised as an image, then accesses it directly through the browser. The server executes the PHP, and now the attacker has a shell on your server.
Validating file extensions isn’t enough. An attacker can name a PHP file `malicious.php.jpg` or craft a file that has a valid image extension but contains executable code in metadata or appended content.
You need multiple layers:
Check MIME types using `wp_check_filetype()` and `wp_check_filetype_and_ext()`. These verify the file type matches what you expect based on magic numbers and file content, not just the extension.
Validate file contents for images using `getimagesize()`. This reads the file header to confirm it’s actually an image. If it returns false, reject the file.
Restrict upload locations. Store uploads outside the web root if possible, or at minimum in a directory with an `.htaccess` file that blocks script execution:
“`
deny from all
“`
Rename files on upload to strip any executable extensions. Generate a unique filename using `wp_unique_filename()` and save it with a safe extension.
A logistics client had a custom shipment tracking plugin that let admins upload proof-of-delivery photos. The upload handler checked file extensions but not content. An attacker submitted a file named `shell.php.jpg` with PHP code inside. The file passed validation, got saved in an uploads subdirectory that allowed PHP execution, and the attacker had persistent access until the hosting provider caught it.
File uploads are inherently risky. Treat every uploaded file as hostile until you’ve verified it six ways.

Check Capabilities Before Executing Privileged Actions
WordPress has a built-in capability system that defines what each user role can do. Administrators can do almost everything. Editors can publish posts but not install plugins. Subscribers can’t do much at all.
Your plugin needs to respect this system by checking user capabilities before executing any action that changes data or settings.
Use `current_user_can()` to verify permissions:
“`
if ( ! current_user_can( ‘manage_options’ ) ) {
wp_die( ‘You do not have permission to access this page’ );
}
“`
This checks whether the current user has the `manage_options` capability, which is typically reserved for administrators. If they don’t, you block the action.
We’ve seen custom plugins that check `if ( is_admin() )` to restrict access to admin functions. That’s not a capability check. That’s checking whether you’re on an admin page. Any logged-in user can access admin pages — they just can’t see menu items they don’t have permission for. But if they know the URL or the AJAX action name, they can call it directly.
A real example: a client’s custom CRM plugin had an export function that dumped the entire customer database to CSV. The function checked `is_admin()` but not capabilities. Any logged-in user — even subscribers — could call the AJAX action and download the full customer list. The developer assumed “only admins would know the action name.” That’s not security. That’s obscurity.
Every action that changes settings, modifies data, or accesses restricted information needs a capability check. Not a role check. Not an “is admin page” check. A proper capability verification using `current_user_can()`.
Avoid Using Eval and Other Dangerous Functions
Some PHP functions are so risky they should almost never appear in production code. `eval()`, `assert()` with a string argument, `create_function()`, `unserialize()` on untrusted data — these all execute arbitrary code, which means if an attacker can control the input, they control your server.
`eval()` is the most common offender. It takes a string and executes it as PHP code. If any part of that string comes from user input — even indirectly — you’ve created a remote code execution vulnerability.
We found `eval()` in a client’s custom reporting plugin. The developer used it to build dynamic date range calculations based on user-selected options. The input was a dropdown, so “it’s safe.” Except the dropdown values weren’t validated server-side. An attacker modified the POST request to inject code. One HTTP request later, they had shell access.
`unserialize()` is another high-risk function. It reconstructs PHP objects from serialized strings, but if an attacker can control the serialized string, they can instantiate objects with malicious properties or magic methods that execute code.
If you need to serialize data, use `json_encode()` and `json_decode()` instead. JSON can’t contain PHP objects or executable code, which makes it safe by default.
The rule: if a function can execute arbitrary code and accepts external input, don’t use it. Find another approach. There’s always another way to solve the problem that doesn’t involve `eval()`.
Keep Dependencies Updated and Audit Third-Party Code
Your plugin’s security doesn’t stop at the code you write. If you’re using third-party libraries — JavaScript frameworks, PHP packages, API SDKs — you’re responsible for keeping them updated.
Vulnerabilities get discovered in popular libraries all the time. jQuery, Lodash, Moment.js, Guzzle — all have had security issues in past versions. If your plugin bundles an outdated library, you inherit its vulnerabilities.
Use Composer for PHP dependencies and keep a `composer.lock` file to track versions. Run `composer update` regularly to pull in security patches.
Use npm or Yarn for JavaScript dependencies and monitor for security advisories using `npm audit`.
We rebuilt a custom booking plugin that bundled a three-year-old version of a date picker library. The library had a known XSS vulnerability that had been patched two years earlier, but the plugin developer never updated it. The client had been running vulnerable code on a production site for eighteen months. Migrating their plugin to something built by Webcomp Digitex meant starting with an audit, updating every dependency, and implementing a process to keep them current.
If you can’t keep a dependency updated because it breaks your code, that’s a sign your code is fragile. Fix your code or find a different library. Running known-vulnerable dependencies is like leaving your front door unlocked because replacing the lock is inconvenient.
Log Security Events and Monitor for Anomalies
Security isn’t just about prevention. It’s also about detection. If someone does find a way in, you need to know about it quickly.
Log authentication attempts. Failed login attempts with valid usernames. Successful logins from unusual IP addresses. Multiple failed attempts in a short period. These patterns can indicate brute-force attacks or credential stuffing.
Log privilege escalation attempts. Any time a user without proper capabilities tries to access a restricted action, log it. If it happens once, maybe they misclicked. If it happens repeatedly, someone’s probing for vulnerabilities.
Log data exports and bulk operations. If your plugin lets users export data, download files, or perform bulk deletes, log who did it and when. If an account gets compromised, you need a trail to figure out what the attacker accessed.
WordPress doesn’t have detailed logging by default, so you’ll need to implement it in your plugin. Write logs to a secure location outside the web root, and rotate them regularly to avoid filling disk space.
We worked with a healthcare client whose custom patient portal plugin had no logging. When they suspected unauthorized access, there was no way to verify what happened or who accessed what records. We added logging to every patient record view, every data export, every settings change. Two months later, an employee left the company and apparently shared their credentials. Because we had logs, the client could verify exactly which records were accessed and meet their breach notification requirements.
Logging isn’t paranoia. It’s operational awareness. You can’t protect what you can’t see.
Test Plugins with Security Scanners Before Deployment
Manual code review catches a lot, but automated tools catch things humans miss. Before deploying any custom plugin, run it through security scanning tools.
PHP Code Sniffer checks your code against WordPress coding standards and security best practices. It flags direct database queries without `$wpdb->prepare()`, missing nonce checks, unsafe output, and more. It’s not perfect, but it catches obvious issues fast.
Psalm and PHPStan are static analysis tools that identify type errors, potential null pointer issues, and other bugs that can lead to security problems. They’re stricter than CodeSniffer and can catch logic errors that CodeSniffer misses.
WPScan is specifically built to scan WordPress sites for vulnerabilities. It checks plugins, themes, and core against a database of known issues. Run it against a staging environment with your custom plugin installed to see if it flags anything.
Automated tools won’t catch everything. They can’t assess business logic flaws or identify authorization issues specific to your plugin’s functionality. But they’ll find the low-hanging fruit — XSS in output, SQL injection in queries, missing capability checks — fast.
We’ve caught vulnerabilities in our own code using these tools. They’re not a replacement for manual review or penetration testing, but they’re a good first pass that takes minutes instead of hours.
At Webcomp Digitex, we run PHP Code Sniffer and static analysis on every plugin before client review. It’s part of the development workflow, not an afterthought. That way issues get caught before they reach staging, not after they’ve been live for six months.
Establish a Process for Handling Security Disclosures
Eventually, someone’s going to find a vulnerability in your plugin. A security researcher. A client’s internal team. A well-meaning hacker. When that happens, you need a plan.
Provide a clear contact method for security issues. Don’t make researchers hunt for a way to report vulnerabilities. Include a security email or contact form in your plugin documentation and README files.
Respond promptly. Security researchers expect acknowledgment within 24–48 hours. If you ignore them, they’ll either disclose publicly or move on. Neither outcome helps you.
Fix critical issues immediately. If the vulnerability allows remote code execution, SQL injection, or privilege escalation, drop everything and fix it. A patch that takes days instead of hours can mean the difference between a controlled disclosure and active exploitation in the wild.
Communicate with affected users. If a vulnerability gets exploited before you patch it, clients need to know so they can take action — change passwords, audit logs, notify users. Staying silent makes it worse.
We handle security disclosures for every plugin we build. A researcher once reported an XSS vulnerability in a custom client portal we’d developed. We acknowledged within two hours, deployed a patch within six, and notified the client with clear remediation steps within twelve. The researcher published a responsible disclosure crediting us for the fast response. That’s how it should work.
Security issues aren’t a sign of incompetence. They’re a reality of software development. What matters is how you handle them.
Frequently Asked Questions
What’s the most common WordPress plugin vulnerability we should watch for?
Cross-site scripting from unescaped output. Most plugins handle input sanitization but forget to escape when displaying data. Always use `esc_html()`, `esc_attr()`, and similar functions every time you output user-generated content.
Do nonces really prevent all CSRF attacks effectively?
They prevent attacks from external sites, which is 95% of CSRF risks. They don’t stop attacks if an attacker already has XSS on your site or can intercept nonce values, so nonces work best as one layer in a broader security approach.
Should we avoid certain PHP functions completely in plugin development?
Yes. Never use `eval()`, `assert()` with strings, `create_function()`, or `unserialize()` on untrusted data. These execute arbitrary code and are nearly impossible to secure if an attacker controls the input. Use JSON for serialization instead.
How often should custom WordPress plugins be audited for security?
Audit plugins before initial deployment, after any major feature addition, and at least annually for production plugins. Also audit immediately if a similar plugin or library announces a vulnerability — patterns spread across codebases.
Get Your Custom Plugin Security Right from the Start
Building a custom WordPress plugin that works isn’t enough. It needs to work securely, handle hostile input correctly, and resist common attack patterns from day one.
If you’re developing a plugin in-house or working with an agency, make sure WordPress plugin security is part of the spec, not an afterthought. Validation, sanitization, prepared statements, capability checks, nonce verification — these belong in the first draft, not the third revision.
Webcomp Digitex builds custom WordPress plugins for businesses that can’t afford security incidents. We build validation and escaping into the code structure, audit dependencies before deployment, and run automated security scans before anything goes live. If you need custom functionality without custom vulnerabilities, let’s talk.
Call +91 9960802498 or email digitalmarketing@webcompdigitex.com to discuss your project.


